Merge remote-tracking branch 'TWoWPfui/master'

This commit is contained in:
Mats391
2026-02-22 13:21:02 +01:00
43 changed files with 9017 additions and 1094 deletions
+1407 -29
View File
File diff suppressed because it is too large Load Diff
+129 -4
View File
@@ -3,6 +3,82 @@ pfUI.api = { }
-- load pfUI environment
setfenv(1, pfUI:GetEnvironment())
-- [ DLL Detection Helpers ]
-- Detects presence of various DLL extensions for enhanced functionality
-- [ HasSuperWoW ]
-- Returns true if SuperWoW DLL is active
-- SuperWoW provides: UNIT_CASTEVENT, UnitPosition, SetMouseoverUnit, SpellInfo, etc.
function pfUI.api.HasSuperWoW()
return SUPERWOW_VERSION or (SetAutoloot and SpellInfo)
end
-- [ HasUnitXP ]
-- Returns true if UnitXP_SP3 DLL is active
-- UnitXP provides: distance, line of sight, behind detection, targeting helpers
function pfUI.api.HasUnitXP()
local success = pcall(UnitXP, "nop", "nop")
return success
end
-- [ HasNampower ]
-- Returns true if Nampower DLL is active
-- Nampower provides: spell queuing, GetCastInfo, GetSpellIdCooldown, IsSpellInRange, etc.
function pfUI.api.HasNampower()
return GetNampowerVersion and true or false
end
-- [ GetUnitDistance ]
-- Returns distance to unit using best available method
-- 'unit1' [string] first unit (default: "player")
-- 'unit2' [string] second unit
-- returns: [number] distance in yards, or nil if unavailable
function pfUI.api.GetUnitDistance(unit1, unit2)
if not unit2 then
unit2 = unit1
unit1 = "player"
end
if not UnitExists(unit2) then return nil end
-- Try UnitXP first (most accurate)
if pfUI.api.HasUnitXP() then
local success, distance = pcall(UnitXP, "distanceBetween", unit1, unit2)
if success and distance then return distance end
end
-- Try SuperWoW UnitPosition
if pfUI.api.HasSuperWoW() and UnitPosition then
local x1, y1, z1 = UnitPosition(unit1)
local x2, y2, z2 = UnitPosition(unit2)
if x1 and y1 and z1 and x2 and y2 and z2 then
return ((x2 - x1)^2 + (y2 - y1)^2 + (z2 - z1)^2)^0.5
end
end
return nil
end
-- [ UnitInLineOfSight ]
-- Returns true if unit1 has line of sight to unit2
-- Requires UnitXP_SP3
function pfUI.api.UnitInLineOfSight(unit1, unit2)
if not pfUI.api.HasUnitXP() then return nil end
local success, inSight = pcall(UnitXP, "inSight", unit1, unit2)
if success then return inSight end
return nil
end
-- [ UnitIsBehind ]
-- Returns true if unit1 is behind unit2
-- Requires UnitXP_SP3
function pfUI.api.UnitIsBehind(unit1, unit2)
if not pfUI.api.HasUnitXP() then return nil end
local success, behind = pcall(UnitXP, "behind", unit1, unit2)
if success then return behind end
return nil
end
-- Client API shortcuts
gfind = string.gmatch or string.gfind
mod = math.mod or mod
@@ -369,18 +445,33 @@ end
-- 'number' [number] the number that should be abbreviated
-- 'returns: [string] the abbreviated value
function pfUI.api.Abbreviate(number)
if pfUI_config.unitframes.abbrevnum == "1" then
local mode = pfUI_config.unitframes.abbrevnum
-- mode "0" = disabled (full numbers)
-- mode "1" = 2 decimals (4250 -> 4.25k) [legacy/default]
-- mode "2" = 1 decimal (4250 -> 4.2k) - always rounds DOWN
if mode == "1" or mode == "2" then
local sign = number < 0 and -1 or 1
number = math.abs(number)
if number > 1000000 then
return pfUI.api.round(number/1000000*sign,2) .. "m"
if mode == "2" then
-- 1 decimal, round DOWN: 4.18m -> 4.1m
return (floor(number/100000) / 10 * sign) .. "m"
else
return pfUI.api.round(number/1000000*sign, 2) .. "m"
end
elseif number > 1000 then
return pfUI.api.round(number/1000*sign,2) .. "k"
if mode == "2" then
-- 1 decimal, round DOWN: 4180 -> 4.1k (not 4.2k)
return (floor(number/100) / 10 * sign) .. "k"
else
return pfUI.api.round(number/1000*sign, 2) .. "k"
end
end
end
return number
return math.floor(number)
end
-- [ SendChatMessageWide ]
@@ -1184,6 +1275,10 @@ function pfUI.api.EnableAutohide(frame, timeout, combat)
end
frame.hover:SetScript("OnUpdate", function()
-- throttle to 0.05s
if (this.tick or 0) > GetTime() then return end
this.tick = GetTime() + 0.05
if this.activeTo == "keep" then return end
if MouseIsOver(this, 10, -10, -10, 10) then
@@ -1371,3 +1466,33 @@ function pfUI.api.GetNoNameObject(frame, objtype, layer, arg1, arg2)
end
end
end
-- [ TryMemoizedFuncLoadstringForSpellCasts ]
-- Memoizes lua function strings for spell casts to improve performance.
-- Supports both string functions and direct function values.
-- 'funcOrStr' [function|string] Either a function or a lua string to execute
-- return: [function|nil] The function to execute, or nil on error
local memoizedFuncs = {}
function pfUI.api.TryMemoizedFuncLoadstringForSpellCasts(funcOrStr)
-- If it's already a function, return it directly
if type(funcOrStr) == "function" then
return funcOrStr
end
-- If it's a string, try to memoize it
if type(funcOrStr) == "string" then
-- Check if we've already compiled this string
if memoizedFuncs[funcOrStr] then
return memoizedFuncs[funcOrStr]
end
-- Try to compile the string
local func = loadstring(funcOrStr)
if func then
memoizedFuncs[funcOrStr] = func
return func
end
end
return nil
end
+48 -6
View File
@@ -148,7 +148,6 @@ function pfUI:LoadConfig()
pfUI:UpdateConfig("appearance", "cd", "font_size", "12")
pfUI:UpdateConfig("appearance", "cd", "font_size_blizz", "12")
pfUI:UpdateConfig("appearance", "cd", "font_size_foreign","12")
pfUI:UpdateConfig("appearance", "cd", "debuffs", "1")
pfUI:UpdateConfig("appearance", "cd", "blizzard", "1")
pfUI:UpdateConfig("appearance", "cd", "foreign", "0")
pfUI:UpdateConfig("appearance", "cd", "milliseconds", "1")
@@ -219,14 +218,48 @@ function pfUI:LoadConfig()
pfUI:UpdateConfig("unitframes", nil, "rangecheck", "0")
pfUI:UpdateConfig("unitframes", nil, "buffdetect", "0")
pfUI:UpdateConfig("unitframes", nil, "druidmanabar", "1")
pfUI:UpdateConfig("unitframes", nil, "druidmanaheight", "2")
pfUI:UpdateConfig("unitframes", nil, "druidmanatext", "0")
pfUI:UpdateConfig("unitframes", nil, "druidmanaheight", "10")
pfUI:UpdateConfig("unitframes", nil, "druidmanawidth", "-1")
pfUI:UpdateConfig("unitframes", nil, "druidmanaoffx", "0")
pfUI:UpdateConfig("unitframes", nil, "druidmanaoffy", "0")
pfUI:UpdateConfig("unitframes", nil, "druidmanaspace", "-3")
pfUI:UpdateConfig("unitframes", nil, "druidmanatexture", "Interface\\AddOns\\pfUI\\img\\bar")
pfUI:UpdateConfig("unitframes", nil, "rangechecki", "4")
pfUI:UpdateConfig("unitframes", nil, "combowidth", "6")
pfUI:UpdateConfig("unitframes", nil, "comboheight", "6")
pfUI:UpdateConfig("unitframes", nil, "swingtimerwidth", "200")
pfUI:UpdateConfig("unitframes", nil, "swingtimerheight", "12")
pfUI:UpdateConfig("unitframes", nil, "swingtimertexture", "Interface\\AddOns\\pfUI\\img\\bar")
pfUI:UpdateConfig("unitframes", nil, "swingtimertext", "1")
pfUI:UpdateConfig("unitframes", nil, "swingtimerlabel", "1")
pfUI:UpdateConfig("unitframes", nil, "swingtimeroffhand","1")
pfUI:UpdateConfig("unitframes", nil, "swingtimerranged", "1")
pfUI:UpdateConfig("unitframes", nil, "swingtimerfontsize","12")
pfUI:UpdateConfig("unitframes", nil, "swingtimermhcolor",".8,.3,.3,1")
pfUI:UpdateConfig("unitframes", nil, "swingtimerohcolor",".3,.8,.3,1")
pfUI:UpdateConfig("unitframes", nil, "swingtimerrangedcolor",".3,.6,1,1")
pfUI:UpdateConfig("unitframes", nil, "swingtimerrangedwarncolor",".9,0,0,1")
pfUI:UpdateConfig("unitframes", nil, "swingtimerhsqueue","1")
pfUI:UpdateConfig("unitframes", nil, "abbrevnum", "1")
pfUI:UpdateConfig("unitframes", nil, "castbardecimals", "2")
pfUI:UpdateConfig("unitframes", nil, "abbrevname", "1")
-- Nampower Settings
pfUI:UpdateConfig("unitframes", nil, "spellqueue", "1")
pfUI:UpdateConfig("unitframes", nil, "spellqueuesize", "24")
pfUI:UpdateConfig("unitframes", nil, "gcd_indicator", "0")
pfUI:UpdateConfig("unitframes", nil, "gcd_size", "4")
pfUI:UpdateConfig("unitframes", nil, "reactive_indicator", "0")
pfUI:UpdateConfig("unitframes", nil, "reactive_size", "28")
pfUI:UpdateConfig("unitframes", nil, "damage_tracking", "0")
-- UnitXP Settings
pfUI:UpdateConfig("unitframes", nil, "los_indicator", "0")
pfUI:UpdateConfig("unitframes", nil, "behind_indicator", "0")
pfUI:UpdateConfig("unitframes", nil, "unitxp_notify", "0")
pfUI:UpdateConfig("unitframes", nil, "track_group", "0")
pfUI:UpdateConfig("unitframes", nil, "selfingroup", "0")
pfUI:UpdateConfig("unitframes", nil, "selfinraid", "0")
pfUI:UpdateConfig("unitframes", nil, "raidforgroup", "0")
@@ -697,6 +730,8 @@ function pfUI:LoadConfig()
pfUI:UpdateConfig("tooltip", nil, "font_tooltip", "Interface\\AddOns\\pfUI\\fonts\\Myriad-Pro.ttf")
pfUI:UpdateConfig("tooltip", nil, "font_tooltip_size", "12")
-- Throttle Settings
pfUI:UpdateConfig("chat", "text", "input_width", "0")
pfUI:UpdateConfig("chat", "text", "input_height", "0")
pfUI:UpdateConfig("chat", "text", "outline", "1")
@@ -714,6 +749,7 @@ function pfUI:LoadConfig()
pfUI:UpdateConfig("chat", "text", "detecturl", "1")
pfUI:UpdateConfig("chat", "text", "classcolor", "1")
pfUI:UpdateConfig("chat", "text", "whosearchunknown", "0")
pfUI:UpdateConfig("chat", "text", "playerlevel", "0")
pfUI:UpdateConfig("chat", "left", "width", "380")
pfUI:UpdateConfig("chat", "left", "height", "180")
pfUI:UpdateConfig("chat", "right", "enable", "0")
@@ -737,9 +773,11 @@ function pfUI:LoadConfig()
pfUI:UpdateConfig("chat", "bubbles", "borders", "1")
pfUI:UpdateConfig("chat", "bubbles", "alpha", ".75")
pfUI:UpdateConfig("nameplates", nil, "showhostile", "1")
pfUI:UpdateConfig("nameplates", nil, "showfriendly", "0")
pfUI:UpdateConfig("nameplates", nil, "use_unitfonts", "0")
pfUI:UpdateConfig("nameplates", nil, "showhostile", "1")
pfUI:UpdateConfig("nameplates", nil, "showfriendly", "0")
pfUI:UpdateConfig("nameplates", nil, "disable_hostile_in_friendly", "0")
pfUI:UpdateConfig("nameplates", nil, "disable_friendly_in_friendly", "0")
pfUI:UpdateConfig("nameplates", nil, "use_unitfonts", "0")
pfUI:UpdateConfig("nameplates", nil, "legacy", "0")
pfUI:UpdateConfig("nameplates", nil, "overlap", "0")
pfUI:UpdateConfig("nameplates", nil, "verticalhealth", "0")
@@ -818,6 +856,9 @@ function pfUI:LoadConfig()
pfUI:UpdateConfig("nameplates", "debuffs", "blacklist", "")
pfUI:UpdateConfig("nameplates", "debuffs", "showstacks", "0")
pfUI:UpdateConfig("nameplates", "debuffs", "position", "BOTTOM")
pfUI:UpdateConfig("nameplates", nil, "debufftimers", "1")
pfUI:UpdateConfig("nameplates", nil, "debufftext", "1")
pfUI:UpdateConfig("nameplates", nil, "debuffanim", "0")
pfUI:UpdateConfig("abuttons", nil, "enable", "1")
pfUI:UpdateConfig("abuttons", nil, "position", "bottom")
@@ -862,6 +903,7 @@ function pfUI:LoadConfig()
pfUI:UpdateConfig("thirdparty", "bcs", "enable", "1")
pfUI:UpdateConfig("thirdparty", "crafty", "enable", "1")
pfUI:UpdateConfig("thirdparty", "clevermacro", "enable", "1")
pfUI:UpdateConfig("thirdparty", "supercleveroidmacros", "enable", "1")
pfUI:UpdateConfig("thirdparty", "flightmap", "enable", "1")
pfUI:UpdateConfig("thirdparty", "sheepwatch", "enable", "1")
pfUI:UpdateConfig("thirdparty", "totemtimers", "enable", "1")
+885 -176
View File
File diff suppressed because it is too large Load Diff
+32
View File
@@ -878,4 +878,36 @@ pfUI_translation["deDE"] = {
["Zonetime"] = nil,
["Zoom & Fade"] = nil,
["Zoom Target Nameplate"] = nil,
-- Throttling translations
["Throttling"] = "Drosselung",
["Nameplates"] = "Namensplaketten",
["Tooltips"] = "Tooltips",
["Chat Tab"] = "Chat Tab",
["Libpredict"] = "Libpredict",
["Panel Alignment"] = "Panel Ausrichtung",
["Nameplate Update Rate"] = "Namensplaketten Aktualisierungsrate",
["Target/Casting Plates"] = "Ziel/Cast Plaketten",
["Normal Plates"] = "Normale Plaketten",
["Mass Pulls (20+ Plates)"] = "Große Pulls (20+ Plaketten)",
["Tooltip Update Rate"] = "Tooltip Aktualisierungsrate",
["Cursor Follow"] = "Cursor Folgen",
["Health/Status Bar"] = "Leben/Status Leiste",
["Unit Frame Update Rate"] = "Einheiten Frame Aktualisierungsrate",
["Raid/Party Frames"] = "Raid/Gruppe Frames",
["Player Frame"] = "Spieler Frame",
["Chat Tab Hover Check"] = "Chat Tab Hover Prüfung",
["Heal Prediction Cleanup"] = "Heilvorhersage Bereinigung",
["Panel Alignment Check"] = "Panel Ausrichtungsprüfung",
["Custom FPS"] = "Benutzerdefiniert FPS",
["Very Slow"] = "Sehr Langsam",
["Slow"] = "Langsam",
["Normal"] = "Normal",
["Fast"] = "Schnell",
["Very Fast"] = "Sehr Schnell",
["Fastest"] = "Schnellste",
["Reset to Defaults"] = "Auf Standard zurücksetzen",
["Target/Casting"] = "Ziel/Cast",
["Normal"] = "Normal",
["Mass"] = "Masse",
}
+32
View File
@@ -878,4 +878,36 @@ pfUI_translation["enUS"] = {
["Zonetime"] = nil,
["Zoom & Fade"] = nil,
["Zoom Target Nameplate"] = nil,
-- Throttling translations
["Throttling"] = nil,
["Nameplates"] = nil,
["Tooltips"] = nil,
["Chat Tab"] = nil,
["Libpredict"] = nil,
["Panel Alignment"] = nil,
["Nameplate Update Rate"] = nil,
["Target/Casting Plates"] = nil,
["Normal Plates"] = nil,
["Mass Pulls (20+ Plates)"] = nil,
["Tooltip Update Rate"] = nil,
["Cursor Follow"] = nil,
["Health/Status Bar"] = nil,
["Unit Frame Update Rate"] = nil,
["Raid/Party Frames"] = nil,
["Player Frame"] = nil,
["Chat Tab Hover Check"] = nil,
["Heal Prediction Cleanup"] = nil,
["Panel Alignment Check"] = nil,
["Custom FPS"] = nil,
["Very Slow"] = nil,
["Slow"] = nil,
["Normal"] = nil,
["Fast"] = nil,
["Very Fast"] = nil,
["Fastest"] = nil,
["Reset to Defaults"] = nil,
["Target/Casting"] = nil,
["Normal"] = nil,
["Mass"] = nil,
}
+1
View File
@@ -700,6 +700,7 @@ pfUI_translation["zhCN"] = {
["Show Empty Buttons"] = "显示空按钮",
["Show FPS and Latency Colors"] = "显示帧数以及延迟颜色",
["Show Guild Name"] = "显示公会名称",
["Show Player Levels"] ="显示玩家等级",
["Show Happiness Icon"] = "显示高兴值图标",
["Show Health Points"] = "显示生命值",
["Show/Hide TimeManager"] = "显示/隐藏时间管理器",
+1
View File
@@ -8,5 +8,6 @@
<Include file="..\libs\libtooltip.lua"/>
<Include file="..\libs\libhealth.lua"/>
<Include file="..\libs\libtotem.lua"/>
<Include file="..\libs\libthrottle.lua"/>
<Include file="..\libs\libpredict.lua"/>
</Ui>
+4
View File
@@ -17,6 +17,7 @@
<Include file="..\modules\focus.lua"/>
<Include file="..\modules\target.lua"/>
<Include file="..\modules\combopoints.lua"/>
<Include file="..\modules\swingtimer.lua"/>
<Include file="..\modules\targettarget.lua"/>
<Include file="..\modules\targettargettarget.lua"/>
<Include file="..\modules\pet.lua"/>
@@ -72,4 +73,7 @@
<Include file="..\modules\macrotweak.lua"/>
<Include file="..\modules\turtle-wow.lua"/>
<Include file="..\modules\superwow.lua"/>
<Include file="..\modules\nampower.lua"/>
<Include file="..\modules\unitxp.lua"/>
<Include file="..\modules\bgscore.lua"/>
</Ui>
+174 -18
View File
@@ -53,14 +53,84 @@ local scanner = libtipscan:GetScanner("libcast")
local libcast = CreateFrame("Frame", "pfEnemyCast")
local player = UnitName("player")
UnitChannelInfo = _G.UnitChannelInfo or function(unit)
-- Store original SuperWoW UnitChannelInfo if it exists
local SuperWoW_UnitChannelInfo = _G.UnitChannelInfo
UnitChannelInfo = function(unit)
-- convert to name if unitstring was given
unit = pfValidUnits[unit] and UnitName(unit) or unit
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 = UnitExists(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 SuperWoW if available, otherwise use libcast.db
if SuperWoW_UnitChannelInfo then
return SuperWoW_UnitChannelInfo(unit)
end
-- 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)
if not db 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
local db = libcast.db[unit]
-- clean legacy values
if db and db.cast and db.start + db.casttime / 1000 > GetTime() then
if not db.channel then return end
cast = db.cast
@@ -71,7 +141,6 @@ UnitChannelInfo = _G.UnitChannelInfo or function(unit)
endTime = startTime + db.casttime
isTradeSkill = nil
elseif db then
-- remove cast action to the database
db.cast = nil
db.rank = nil
db.start = nil
@@ -83,14 +152,85 @@ UnitChannelInfo = _G.UnitChannelInfo or function(unit)
return cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill
end
UnitCastingInfo = _G.UnitCastingInfo or function(unit)
-- Store original SuperWoW UnitCastingInfo if it exists
local SuperWoW_UnitCastingInfo = _G.UnitCastingInfo
UnitCastingInfo = function(unit)
-- convert to name if unitstring was given
unit = pfValidUnits[unit] and UnitName(unit) or unit
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 = UnitExists(unit)
guid = unitGuid
end
-- For player: ALWAYS use libcast.db because it handles pushback correctly
-- SuperWoW's UnitCastingInfo doesn't track SPELLCAST_DELAYED events
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 SuperWoW if available, otherwise use libcast.db
if SuperWoW_UnitCastingInfo then
return SuperWoW_UnitCastingInfo(unit)
end
-- 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)
if not db 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
local db = libcast.db[unit]
-- clean legacy values
if db and db.cast and db.start + db.casttime / 1000 > GetTime() then
if db.channel then return end
cast = db.cast
@@ -101,7 +241,6 @@ UnitCastingInfo = _G.UnitCastingInfo or function(unit)
endTime = startTime + db.casttime
isTradeSkill = nil
elseif db then
-- remove cast action to the database
db.cast = nil
db.rank = nil
db.start = nil
@@ -182,17 +321,31 @@ libcast:RegisterEvent("SPELLCAST_CHANNEL_STOP")
libcast:RegisterEvent("SPELLCAST_CHANNEL_UPDATE")
local mob, spell, icon, _
libcast:SetScript("OnEvent", function()
-- Fill database with player casts
if event == "SPELLCAST_START" then
icon = L["spells"][arg1] and L["spells"][arg1].icon and string.format("%s%s", "Interface\\Icons\\", L["spells"][arg1].icon) or lastcasttex
-- add cast action to the database
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
-- Check if SuperWoW already set the cast data (with correct haste-adjusted casttime)
-- If so, only update icon if needed, don't overwrite casttime
local superWowAlreadySet = this.db[player].cast == arg1 and this.db[player].casttime and this.db[player].casttime > 0
if superWowAlreadySet then
-- SuperWoW already set correct casttime, only update icon if better
if icon and not this.db[player].icon then
this.db[player].icon = icon
end
else
-- No SuperWoW data, use SPELLCAST_START data
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
end
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
@@ -214,7 +367,9 @@ libcast:SetScript("OnEvent", function()
end
elseif event == "SPELLCAST_DELAYED" then
if this.db[player].cast then
this.db[player].start = this.db[player].start + arg1/1000
-- 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
@@ -224,6 +379,7 @@ libcast:SetScript("OnEvent", function()
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
+1794 -178
View File
File diff suppressed because it is too large Load Diff
+666 -98
View File
@@ -9,14 +9,30 @@ setfenv(1, pfUI:GetEnvironment())
-- UnitGetIncomingHeals(unit)
-- UnitHasIncomingResurrection(unit)
--
-- The library is able to receive and send compatible messages to HealComm (vanilla)
-- and HealComm (tbc) including the ressurections of both versions. It has an option
-- to disable the sending of those messages in case one of the mentioned libraries
-- is already active.
-- The library is able to receive and send compatible messages to HealComm
-- including resurrections. It has an option to disable the sending of those
-- messages in case HealComm is already active.
--
-- HOT TRACKING INTEGRATION (NEW):
-- With Nampower enabled, HoT tracking now primarily uses libdebuff's AURA_CAST
-- event system for accurate server-side buff/debuff tracking with full rank
-- protection. GetHotDuration() first checks libdebuff, then falls back to the
-- legacy prediction system for backwards compatibility with non-Nampower clients.
-- This provides:
-- - Accurate duration from server (no prediction needed)
-- - Automatic rank protection (lower ranks won't overwrite higher ranks)
-- - Support for multiple casters of same HoT on one target
-- - Zero event overhead (libdebuff already tracks all auras)
-- return instantly when another libpredict is already active
if pfUI.api.libpredict then return end
-- Check if libdebuff integration is available
local libdebuff_available = (pfUI.api.libdebuff and pfUI.api.libdebuff.GetBestAuraCast) and true or false
-- Check if Nampower is available for SPELL_FAILED events
local hasNampower = GetNampowerVersion ~= nil
local senttarget
local heals, ress, events, hots = {}, {}, {}, {}
@@ -80,35 +96,168 @@ do -- Regrowth
REGROWTH = locales[GetLocale()] or locales["enUS"]
end
-- SuperWoW detection
local superwow_active = SpellInfo ~= nil
-- Spell IDs für UNIT_CASTEVENT (SuperWoW)
local SPELL_IDS = {
-- Rejuvenation (alle Ränge)
[774] = "Reju", [1058] = "Reju", [1430] = "Reju", [2090] = "Reju", [2091] = "Reju",
[3627] = "Reju", [8910] = "Reju", [9839] = "Reju", [9840] = "Reju", [9841] = "Reju",
[25299] = "Reju", [26981] = "Reju", [26982] = "Reju",
-- Renew (alle Ränge)
[139] = "Renew", [6074] = "Renew", [6075] = "Renew", [6076] = "Renew", [6077] = "Renew",
[6078] = "Renew", [10927] = "Renew", [10928] = "Renew", [10929] = "Renew", [25315] = "Renew",
[25221] = "Renew", [25222] = "Renew",
}
local libpredict = CreateFrame("Frame")
libpredict:RegisterEvent("UNIT_HEALTH")
libpredict:RegisterEvent("CHAT_MSG_ADDON")
libpredict:RegisterEvent("PLAYER_TARGET_CHANGED")
libpredict:RegisterEvent("PLAYER_LOGOUT")
-- SuperWoW: Registriere UNIT_CASTEVENT für akkurate Instant-HoT Detection
if superwow_active then
libpredict:RegisterEvent("UNIT_CASTEVENT")
end
libpredict:SetScript("OnEvent", function()
-- Handle shutdown to prevent crash 132
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
return
end
if event == "CHAT_MSG_ADDON" and (arg1 == "HealComm" or arg1 == "CTRA") then
this:ParseChatMessage(arg4, arg2, arg1)
elseif event == "UNIT_HEALTH" then
local name = UnitName(arg1)
if ress[name] and not UnitIsDeadOrGhost(arg1) then
ress[UnitName(arg1)] = nil
if name and ress[name] and not UnitIsDeadOrGhost(arg1) then
ress[name] = nil -- Reuse 'name' variable instead of calling UnitName again
end
elseif event == "UNIT_CASTEVENT" and superwow_active then
-- arg1 = casterGUID, arg2 = targetGUID, arg3 = event type, arg4 = spellId, arg5 = castTime
local casterGUID, targetGUID, castEvent, spellId = arg1, arg2, arg3, arg4
-- Nur eigene Casts (player)
local _, playerGUID = UnitExists("player")
if casterGUID ~= playerGUID then return end
-- Nur "CAST" events (erfolgreiche Instant-Casts)
if castEvent ~= "CAST" then return end
-- Prüfe ob es ein Instant-HoT ist
local hotType = SPELL_IDS[spellId]
if not hotType then return end
-- Finde Target Name
local targetName
for i = 1, 40 do
local unit = "raid" .. i
if UnitExists(unit) then
local _, guid = UnitExists(unit)
if guid == targetGUID then
targetName = UnitName(unit)
break
end
end
end
if not targetName then
for i = 1, 4 do
local unit = "party" .. i
if UnitExists(unit) then
local _, guid = UnitExists(unit)
if guid == targetGUID then
targetName = UnitName(unit)
break
end
end
end
end
if not targetName then
local _, guid = UnitExists("player")
if guid == targetGUID then
targetName = UnitName("player")
end
end
if not targetName then
local _, guid = UnitExists("target")
if guid == targetGUID then
targetName = UnitName("target")
end
end
if not targetName then return end
-- Duration bestimmen
local duration
if hotType == "Reju" then
duration = rejuvDuration or 12
elseif hotType == "Renew" then
duration = renewDuration or 15
end
-- Extract rank from spellId (if SpellInfo available)
local rank = nil
if SpellInfo then
local _, rankString = SpellInfo(spellId)
if rankString and rankString ~= "" then
rank = tonumber((string.gsub(rankString, "Rank ", ""))) or nil
end
end
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ffff[UNIT_CASTEVENT]|r spell=%s target=%s dur=%s rank=%s",
hotType, targetName, tostring(duration), tostring(rank or "?")))
end
-- Sende HoT mit Rank
local playerName = UnitName("player")
libpredict:Hot(playerName, targetName, hotType, duration, nil, "UNIT_CASTEVENT", rank)
-- Sende HealComm Nachricht mit Rank (backwards compatible: rank optional)
-- Use "0" for unknown rank instead of empty string to avoid parsing issues
local rankStr = rank and tostring(rank) or "0"
if libpredict.sender and libpredict.sender.SendHealCommMsg then
libpredict.sender:SendHealCommMsg(hotType .. "/" .. targetName .. "/" .. duration .. "/" .. rankStr .. "/")
else
-- Fallback: direkt senden (smart channel selection)
local msg = hotType .. "/" .. targetName .. "/" .. duration .. "/" .. rankStr .. "/"
if GetNumRaidMembers() > 0 then
SendAddonMessage("HealComm", msg, "RAID")
elseif GetNumPartyMembers() > 0 then
SendAddonMessage("HealComm", msg, "PARTY")
end
-- Note: BATTLEGROUND channel not used (no reliable way to detect BG in Vanilla)
end
end
end)
libpredict:SetScript("OnUpdate", function()
-- throttle cleanup - no need to check every frame
local now = pfUI.uf.now or GetTime()
if (this.tick or 0) > now then return end
this.tick = now + pfUI.throttle:Get("libpredict") -- Default: Normal (10 FPS)
-- update on timeout events
for timestamp, targets in pairs(events) do
if GetTime() >= timestamp then
if now >= timestamp then
events[timestamp] = nil
end
end
end)
function libpredict:ParseComm(sender, msg)
local msgtype, target, heal, time
local msgtype, target, heal, time, rank
if msg == "Healstop" or msg == "GrpHealstop" then
if msg == "HealStop" or msg == "Healstop" or msg == "GrpHealstop" then
msgtype = "Stop"
-- DEBUG: Log when HealStop received
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[libpredict RX]|r HealStop from " .. tostring(sender))
end
elseif msg == "Resurrection/stop/" then
msgtype = "RessStop"
elseif msg then
@@ -137,6 +286,13 @@ function libpredict:ParseComm(sender, msg)
if msgobj[1] == "Reju" or msgobj[1] == "Renew" or msgobj[1] == "Regr" then --hots
msgtype, target, heal, time = "Hot", msgobj[2], msgobj[1], msgobj[3]
-- NEW: Parse rank (optional, backwards compatible)
-- Format: "Reju/Target/12/10/" where msgobj[3]=duration, msgobj[4]=rank
-- "0" = unknown rank (for clients without rank extraction)
local rankStr = msgobj[4]
if rankStr and rankStr ~= "" and rankStr ~= "/" and rankStr ~= "0" then
rank = tonumber(rankStr)
end
end
elseif select and UnitCastingInfo then
-- latest healcomm
@@ -166,14 +322,18 @@ function libpredict:ParseComm(sender, msg)
end
end
return msgtype, target, heal, time
return msgtype, target, heal, time, rank
end
-- Duplikat-Erkennung für HoT Nachrichten
local recentHots = {}
local DUPLICATE_WINDOW = 0.5 -- Ignoriere gleiche Nachricht innerhalb 0.5s
function libpredict:ParseChatMessage(sender, msg, comm)
local msgtype, target, heal, time
local msgtype, target, heal, time, rank
if comm == "HealComm" then
msgtype, target, heal, time = libpredict:ParseComm(sender, msg)
msgtype, target, heal, time, rank = libpredict:ParseComm(sender, msg)
elseif comm == "CTRA" then
local _, _, cmd, ctratarget = string.find(msg, "(%a+)%s?([^#]*)")
if cmd and ctratarget and cmd == "RES" and ctratarget ~= "" and ctratarget ~= UNKNOWN then
@@ -201,7 +361,44 @@ function libpredict:ParseChatMessage(sender, msg, comm)
elseif msgtype == "Ress" then
libpredict:Ress(sender, target)
elseif msgtype == "Hot" then
libpredict:Hot(sender, target, heal, time)
-- Duplikat-Check: gleicher sender+target+spell innerhalb DUPLICATE_WINDOW ignorieren
local now = pfUI.uf.now or GetTime()
local key = sender .. target .. heal
if recentHots[key] and (now - recentHots[key]) < DUPLICATE_WINDOW then
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[DUPLICATE IGNORED]|r " .. key)
end
return
end
recentHots[key] = now
-- Cleanup alte Einträge (alle 10s)
if not libpredict.lastCleanup or (now - libpredict.lastCleanup) > 10 then
for k, v in pairs(recentHots) do
if (now - v) > DUPLICATE_WINDOW then
recentHots[k] = nil
end
end
libpredict.lastCleanup = now
end
-- Für eigene HoTs: Korrigiere die startTime
if sender == UnitName("player") then
local existing = hots[target] and hots[target][heal]
-- Wenn bereits ein aktiver Timer existiert, nicht überschreiben
if existing and (existing.start + existing.duration) > now then
return
end
-- Kompensiere HealComm Verzögerung
local delay = (heal == "Regr") and 0.3 or 0
local correctedStart = now - delay
libpredict:Hot(sender, target, heal, time, correctedStart, "ParseComm-Self", rank)
return
end
libpredict:Hot(sender, target, heal, time, nil, "ParseComm", rank)
end
end
@@ -215,24 +412,70 @@ function libpredict:Heal(sender, target, amount, duration)
return
end
local timeout = duration/1000 + GetTime()
local now = pfUI.uf.now or GetTime()
local timeout = duration/1000 + now
heals[target] = heals[target] or {}
heals[target][sender] = { amount, timeout }
libpredict:AddEvent(timeout, target)
end
function libpredict:Hot(sender, target, spell, duration)
-- Debug flag
libpredict.debug = false
function libpredict:Hot(sender, target, spell, duration, startTime, source, rank)
hots[target] = hots[target] or {}
hots[target][spell] = hots[target][spell] or {}
-- Korrigiere Regrowth Duration (Server gibt 21 zurück, sollte aber 20 sein)
if spell == "Regr" then
duration = 20
end
-- Sicherstellen dass duration eine Zahl ist
duration = tonumber(duration) or duration
-- Rank protection: Don't overwrite higher rank HoT with lower rank
local existing = hots[target][spell]
if existing and existing.rank and rank then
local existingRank = tonumber(existing.rank) or 0
local newRank = tonumber(rank) or 0
local now = pfUI.uf.now or GetTime()
local existingTimeleft = (existing.start + existing.duration) - now
-- If existing HoT is still active and has higher rank, don't overwrite
if existingTimeleft > 0 and newRank > 0 and newRank < existingRank then
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cffff0000[Hot RANK BLOCK]|r %s Rank %d cannot overwrite Rank %d on %s",
spell, newRank, existingRank, target))
end
return -- Don't overwrite!
end
end
local now = pfUI.uf.now or GetTime()
hots[target][spell].duration = duration
hots[target][spell].start = GetTime()
hots[target][spell].start = startTime or now
hots[target][spell].rank = rank -- Store rank for protection
-- Debug
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cff33ffcc[Hot]|r src=" .. (source or "?") ..
" | sender=" .. (sender or "nil") ..
" | target=" .. (target or "nil") ..
" | spell=" .. (spell or "nil") ..
" | dur=" .. tostring(duration) .. " (" .. type(duration) .. ")" ..
" | rank=" .. tostring(rank or "?"))
end
-- update aura events of relevant unitframes
if pfUI and pfUI.uf and pfUI.uf.frames then
for _, frame in pairs(pfUI.uf.frames) do
if frame.namecache == target then
frame.update_aura = true
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(" |cff00ff00-> Frame update triggered for " .. (frame:GetName() or "?") .. "|r")
end
end
end
end
@@ -276,16 +519,18 @@ function libpredict:RessStop(sender)
end
function libpredict:UnitGetIncomingHeals(unit)
if not unit or not UnitName(unit) then return 0 end
if UnitIsDeadOrGhost(unit) then return 0 end
if not unit then return 0 end
local name = UnitName(unit)
if not name then return 0 end
if UnitIsDeadOrGhost(unit) then return 0 end
local sumheal = 0
if not heals[name] then
return sumheal
else
local now = pfUI.uf.now or GetTime()
for sender, amount in pairs(heals[name]) do
if amount[2] <= GetTime() then
if amount[2] <= now then
heals[name][sender] = nil
else
sumheal = sumheal + amount[1]
@@ -296,8 +541,9 @@ function libpredict:UnitGetIncomingHeals(unit)
end
function libpredict:UnitHasIncomingResurrection(unit)
if not unit or not UnitName(unit) then return nil end
if not unit then return nil end
local name = UnitName(unit)
if not name then return nil end
if not ress[name] then
return nil
@@ -389,6 +635,23 @@ local function UpdateCache(spell, heal, crit)
end
end
-- Cooldown für lokale Instant-HoT Hooks (verhindert Spam bei Click-to-Cast)
local instantHotCooldown = {}
local INSTANT_HOT_COOLDOWN = 1.0 -- 1 Sekunde Cooldown (GCD ist 1.5s)
-- Pending HoTs Queue - wird nach Delay verifiziert
local pendingHots = {}
-- Hilfsfunktion: Prüfe ob Buff auf Unit vorhanden ist
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
@@ -397,6 +660,52 @@ hooksecurefunc("CastSpell", function(id, bookType)
spell_queue[1] = effect
spell_queue[2] = effect.. ( rank or "" )
spell_queue[3] = UnitName("target") and UnitCanAssist("player", "target") and UnitName("target") or UnitName("player")
-- Extract rank number
local rankNum = nil
if rank and rank ~= "" then
rankNum = tonumber((string.gsub(rank, "Rank ", ""))) or nil
end
-- Instant-HoTs: Mit SuperWoW nutzen wir UNIT_CASTEVENT (akkurater)
-- Ohne SuperWoW: Fallback auf Hook-Methode mit Cooldown
if superwow_active then return end
if effect == REJUVENATION then
local target = spell_queue[3]
local now = pfUI.uf.now or GetTime()
local key = "Reju" .. target
-- Cooldown-Check
if instantHotCooldown[key] and (now - instantHotCooldown[key]) < INSTANT_HOT_COOLDOWN then
return
end
instantHotCooldown[key] = now
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[CastSpell REJU INSTANT]|r target=%s rank=%s (Fallback)", target, tostring(rankNum or "?")))
end
libpredict:Hot(player, target, "Reju", rejuvDuration, nil, "CastSpell-Instant", rankNum)
local rankStr = rankNum and tostring(rankNum) or "0"
libpredict.sender:SendHealCommMsg("Reju/"..target.."/"..rejuvDuration.."/"..rankStr.."/")
elseif effect == RENEW then
local target = spell_queue[3]
local now = pfUI.uf.now or GetTime()
local key = "Renew" .. target
-- Cooldown-Check
if instantHotCooldown[key] and (now - instantHotCooldown[key]) < INSTANT_HOT_COOLDOWN then
return
end
instantHotCooldown[key] = now
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[CastSpell RENEW INSTANT]|r target=%s rank=%s (Fallback)", target, tostring(rankNum or "?")))
end
libpredict:Hot(player, target, "Renew", renewDuration, nil, "CastSpell-Instant", rankNum)
local rankStr = rankNum and tostring(rankNum) or "0"
libpredict.sender:SendHealCommMsg("Renew/"..target.."/"..renewDuration.."/"..rankStr.."/")
end
end)
hooksecurefunc("CastSpellByName", function(effect, target)
@@ -412,9 +721,59 @@ hooksecurefunc("CastSpellByName", function(effect, target)
target = target and target == true and UnitName("player") or target
target = target and target == 1 and UnitName("player") or target
spell_queue[1] = effect
spell_queue[2] = effect.. ( rank or "" )
spell_queue[3] = target or mouseover or default
-- Extract rank number
local rankNum = nil
if rank and rank ~= "" then
rankNum = tonumber((string.gsub(rank, "Rank ", ""))) or nil
end
-- Nur spell_queue überschreiben wenn kein Cast läuft
-- (verhindert dass Instant-Spam während Regrowth-Cast die Queue zerstört)
if not libpredict.sender.current_cast then
spell_queue[1] = effect
spell_queue[2] = effect.. ( rank or "" )
spell_queue[3] = target or mouseover or default
end
-- Instant-HoTs: Mit SuperWoW nutzen wir UNIT_CASTEVENT (akkurater)
-- Ohne SuperWoW: Fallback auf Hook-Methode mit Cooldown
if superwow_active then return end
if effect == REJUVENATION then
local hotTarget = target or mouseover or default
local now = pfUI.uf.now or GetTime()
local key = "Reju" .. hotTarget
-- Cooldown-Check
if instantHotCooldown[key] and (now - instantHotCooldown[key]) < INSTANT_HOT_COOLDOWN then
return
end
instantHotCooldown[key] = now
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[CastSpellByName REJU INSTANT]|r target=%s rank=%s (Fallback)", hotTarget, tostring(rankNum or "?")))
end
libpredict:Hot(player, hotTarget, "Reju", rejuvDuration, nil, "CastSpellByName-Instant", rankNum)
local rankStr = rankNum and tostring(rankNum) or "0"
libpredict.sender:SendHealCommMsg("Reju/"..hotTarget.."/"..rejuvDuration.."/"..rankStr.."/")
elseif effect == RENEW then
local hotTarget = target or mouseover or default
local now = pfUI.uf.now or GetTime()
local key = "Renew" .. hotTarget
-- Cooldown-Check
if instantHotCooldown[key] and (now - instantHotCooldown[key]) < INSTANT_HOT_COOLDOWN then
return
end
instantHotCooldown[key] = now
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[CastSpellByName RENEW INSTANT]|r target=%s rank=%s (Fallback)", hotTarget, tostring(rankNum or "?")))
end
libpredict:Hot(player, hotTarget, "Renew", renewDuration, nil, "CastSpellByName-Instant", rankNum)
local rankStr = rankNum and tostring(rankNum) or "0"
libpredict.sender:SendHealCommMsg("Renew/"..hotTarget.."/"..renewDuration.."/"..rankStr.."/")
end
end)
local scanner = libtipscan:GetScanner("prediction")
@@ -427,41 +786,105 @@ hooksecurefunc("UseAction", function(slot, target, selfcast)
spell_queue[1] = effect
spell_queue[2] = effect.. ( rank or "" )
spell_queue[3] = selfcast and UnitName("player") or UnitName("target") and UnitCanAssist("player", "target") and UnitName("target") or UnitName("player")
-- Extract rank number
local rankNum = nil
if rank and rank ~= "" then
rankNum = tonumber((string.gsub(rank, "Rank ", ""))) or nil
end
-- Instant-HoTs: Mit SuperWoW nutzen wir UNIT_CASTEVENT (akkurater)
-- Ohne SuperWoW: Fallback auf Hook-Methode mit Cooldown
if superwow_active then return end
if effect == REJUVENATION then
local hotTarget = spell_queue[3]
local now = pfUI.uf.now or GetTime()
local key = "Reju" .. hotTarget
-- Cooldown-Check
if instantHotCooldown[key] and (now - instantHotCooldown[key]) < INSTANT_HOT_COOLDOWN then
return
end
instantHotCooldown[key] = now
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[UseAction REJU INSTANT]|r target=%s rank=%s (Fallback)", hotTarget, tostring(rankNum or "?")))
end
libpredict:Hot(player, hotTarget, "Reju", rejuvDuration, nil, "UseAction-Instant", rankNum)
local rankStr = rankNum and tostring(rankNum) or "0"
libpredict.sender:SendHealCommMsg("Reju/"..hotTarget.."/"..rejuvDuration.."/"..rankStr.."/")
elseif effect == RENEW then
local hotTarget = spell_queue[3]
local now = pfUI.uf.now or GetTime()
local key = "Renew" .. hotTarget
-- Cooldown-Check
if instantHotCooldown[key] and (now - instantHotCooldown[key]) < INSTANT_HOT_COOLDOWN then
return
end
instantHotCooldown[key] = now
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[UseAction RENEW INSTANT]|r target=%s rank=%s (Fallback)", hotTarget, tostring(rankNum or "?")))
end
libpredict:Hot(player, hotTarget, "Renew", renewDuration, nil, "UseAction-Instant", rankNum)
local rankStr = rankNum and tostring(rankNum) or "0"
libpredict.sender:SendHealCommMsg("Renew/"..hotTarget.."/"..renewDuration.."/"..rankStr.."/")
end
end)
libpredict.sender = CreateFrame("Frame", "pfPredictionSender", UIParent)
libpredict.sender.enabled = true
libpredict.sender.SendHealCommMsg = function(self, msg)
SendAddonMessage("HealComm", msg, "RAID")
SendAddonMessage("HealComm", msg, "BATTLEGROUND")
-- Smart channel selection: Only send to relevant channel to avoid duplicates
if GetNumRaidMembers() > 0 then
-- In raid: Only send to RAID (includes all raid members)
SendAddonMessage("HealComm", msg, "RAID")
elseif GetNumPartyMembers() > 0 then
-- In party: Only send to PARTY
SendAddonMessage("HealComm", msg, "PARTY")
end
-- Note: BATTLEGROUND channel not used (no reliable way to detect BG in Vanilla)
-- BG groups are handled by RAID channel
end
libpredict.sender.SendResCommMsg = function(self, msg)
SendAddonMessage("CTRA", msg, "RAID")
SendAddonMessage("CTRA", msg, "BATTLEGROUND")
-- Smart channel selection: Only send to relevant channel to avoid duplicates
if GetNumRaidMembers() > 0 then
-- In raid: Only send to RAID (includes all raid members)
SendAddonMessage("CTRA", msg, "RAID")
elseif GetNumPartyMembers() > 0 then
-- In party: Only send to PARTY
SendAddonMessage("CTRA", msg, "PARTY")
end
-- Note: BATTLEGROUND channel not used (no reliable way to detect BG in Vanilla)
-- BG groups are handled by RAID channel
end
libpredict.sender:SetScript("OnUpdate", function()
-- trigger delayed regrowth timers
if this.regrowth_timer and GetTime() > this.regrowth_timer then
local now = pfUI.uf.now or GetTime()
if this.regrowth_timer and now > this.regrowth_timer then
local target = this.regrowth_target or player
local duration = 21
local duration = 20
local startTime = this.regrowth_start
local rank = this.regrowth_rank
libpredict:Hot(player, target, "Regr", duration)
libpredict.sender:SendHealCommMsg("Regr/"..target.."/"..duration.."/")
libpredict:Hot(player, target, "Regr", duration, startTime, "OnUpdate", rank)
local rankStr = rank and tostring(rank) or "0"
libpredict.sender:SendHealCommMsg("Regr/"..target.."/"..duration.."/"..rankStr.."/")
-- Übernehme nächsten Regrowth falls vorhanden
this.regrowth_target = this.regrowth_target_next
this.regrowth_start = this.regrowth_start_next
this.regrowth_rank = this.regrowth_rank_next
this.regrowth_target_next = nil
this.regrowth_start_next = nil
this.regrowth_rank_next = nil
this.regrowth_timer = nil
end
end)
-- tbc
libpredict.sender:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
libpredict.sender:RegisterEvent("UNIT_SPELLCAST_START")
libpredict.sender:RegisterEvent("UNIT_SPELLCAST_STOP")
libpredict.sender:RegisterEvent("UNIT_SPELLCAST_FAILED")
libpredict.sender:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")
libpredict.sender:RegisterEvent("UNIT_SPELLCAST_SENT")
-- vanilla
libpredict.sender:RegisterEvent("CHAT_MSG_SPELL_SELF_BUFF")
libpredict.sender:RegisterEvent("SPELLCAST_START")
libpredict.sender:RegisterEvent("SPELLCAST_STOP")
@@ -469,6 +892,18 @@ libpredict.sender:RegisterEvent("SPELLCAST_FAILED")
libpredict.sender:RegisterEvent("SPELLCAST_INTERRUPTED")
libpredict.sender:RegisterEvent("SPELLCAST_DELAYED")
-- Nampower: Register SPELL_FAILED_SELF for more reliable cast fail detection
if hasNampower then
libpredict.sender:RegisterEvent("SPELL_FAILED_SELF")
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[libpredict]|r Nampower detected - SPELL_FAILED_SELF registered")
end
else
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cffffcc00[libpredict]|r Nampower NOT detected - using vanilla SPELLCAST_FAILED only")
end
end
-- force cache updates
libpredict.sender:RegisterEvent("UNIT_INVENTORY_CHANGED")
libpredict.sender:RegisterEvent("SKILL_LINES_CHANGED")
@@ -498,35 +933,57 @@ libpredict.sender:SetScript("OnEvent", function()
if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal, true) end
return
end
elseif event == "COMBAT_LOG_EVENT_UNFILTERED" and arg2 == "SPELL_HEAL" and arg4 == player then -- tbc
local spell, heal, crit = arg10, arg12, arg13
if spell and heal and crit then
if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal, true) end
elseif spell and heal then
if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal) end
end
elseif event == "UNIT_SPELLCAST_SENT" and arg4 then -- fix tbc mouseover macros
senttarget = arg4
elseif strfind(event, "SPELLCAST_START", 1) then
elseif event == "SPELLCAST_START" then
local spell, time = arg1, arg2
if strfind(event, "UNIT_", 1) then -- tbc
if arg1 ~= "player" then return end
local spellname, _, _, _, starttime, endtime = UnitCastingInfo("player")
spell, time = spellname, endtime - starttime
-- Resolve target from SPELL_CAST_EVENT (via libdebuff) for Nampower queued casts.
-- SPELL_CAST_EVENT fires right before SPELLCAST_START with the actual targetGuid,
-- solving the problem where spell_queue[3] is stale because CastSpellByName hook
-- could not update it while current_cast was set (Nampower spell queuing).
local pending = pfUI.libpredict_pending_cast
local pendingTarget = nil
if pending and pending.spellName == spell and pending.targetGuid
and pending.time and (GetTime() - pending.time) < 1 then
pendingTarget = UnitName(pending.targetGuid)
-- Validate: must be a friendly unit for heal prediction
if pendingTarget and pendingTarget ~= UNKNOWNOBJECT and pendingTarget ~= UKNOWNBEING then
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format(
"|cff00ffff[libpredict]|r SPELL_CAST_EVENT target: %s (guid: %s) for %s",
pendingTarget, pending.targetGuid, spell))
end
else
pendingTarget = nil
end
-- Clear pending after consumption
pending.spellId = nil
pending.spellName = nil
pending.targetGuid = nil
pending.time = nil
end
-- Speichere aktuellen Cast (wird nicht von Instant-Hooks überschrieben)
this.current_cast = spell
this.current_cast_target = pendingTarget or senttarget or spell_queue[3]
if spell_queue[1] == spell and cache[spell_queue[2]] then
local sender = player
local target = senttarget or spell_queue[3]
local target = pendingTarget or senttarget or spell_queue[3]
local amount = cache[spell_queue[2]][1]
local casttime = time
if spell == REGROWTH then
-- Extract rank from spell_queue[2] which contains "spell + rank"
local fullSpell = spell_queue[2]
local _, _, rankStr = fullSpell and string.find(fullSpell, "Rank (%d+)")
local rankNum = rankStr and tonumber(rankStr) or nil
if this.regrowth_timer then
this.regrowth_target_next = spell_queue[3]
this.regrowth_target_next = pendingTarget or spell_queue[3]
this.regrowth_rank_next = rankNum
else
this.regrowth_target = spell_queue[3]
this.regrowth_target = pendingTarget or spell_queue[3]
this.regrowth_rank = rankNum
end
end
@@ -536,88 +993,199 @@ libpredict.sender:SetScript("OnEvent", function()
for i=1,4 do
if CheckInteractDistance("party"..i, 4) then
libpredict:Heal(player, UnitName("party"..i), amount, casttime)
if pfUI.client < 20000 then -- vanilla
libpredict.sender:SendHealCommMsg("Heal/" .. UnitName("party"..i) .. "/" .. amount .. "/" .. casttime .. "/")
else -- tbc
libpredict.sender:SendHealCommMsg(string.format("002%05d%s", math.min(amount, 99999), UnitName("party"..i)))
end
libpredict.sender:SendHealCommMsg("Heal/" .. UnitName("party"..i) .. "/" .. amount .. "/" .. casttime .. "/")
libpredict.sender.healing = true
end
end
end
libpredict:Heal(player, target, amount, casttime)
if pfUI.client < 20000 then -- vanilla
libpredict.sender:SendHealCommMsg("Heal/" .. target .. "/" .. amount .. "/" .. casttime .. "/")
else -- tbc
libpredict.sender:SendHealCommMsg(string.format("002%05d%s", math.min(amount, 99999), target))
end
libpredict.sender:SendHealCommMsg("Heal/" .. target .. "/" .. amount .. "/" .. casttime .. "/")
libpredict.sender.healing = true
elseif spell_queue[1] == spell and L["resurrections"][spell] then
local target = senttarget or spell_queue[3]
local target = pendingTarget or senttarget or spell_queue[3]
libpredict:Ress(player, target)
libpredict.sender:SendHealCommMsg("Resurrection/" .. target .. "/start/")
libpredict.sender:SendResCommMsg("RES " .. target)
libpredict.sender.resurrecting = true
end
elseif strfind(event, "SPELLCAST_FAILED", 1) or strfind(event, "SPELLCAST_INTERRUPTED", 1) then
if strfind(event, "UNIT_", 1) and arg1 ~= "player" then return end
elseif event == "SPELLCAST_FAILED" or event == "SPELLCAST_INTERRUPTED" then
if libpredict.sender.healing then
libpredict:HealStop(player)
if pfUI.client < 20000 then -- vanilla
libpredict.sender:SendHealCommMsg("HealStop")
else -- tbc
libpredict.sender:SendHealCommMsg("001F")
-- DEBUG: Log when sending HealStop
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cffff00ff[libpredict TX]|r Sending HealStop (via " .. event .. ") to group")
end
libpredict.sender:SendHealCommMsg("Healstop")
libpredict.sender.healing = nil
elseif libpredict.sender.resurrecting then
local target = senttarget or spell_queue[3]
local target = this.current_cast_target or senttarget or spell_queue[3]
libpredict:RessStop(player)
libpredict.sender:SendHealCommMsg("Resurrection/stop/")
libpredict.sender:SendResCommMsg("RESNO " .. target)
libpredict.sender.resurrecting = nil
end
if spell_queue[1] == REGROWTH then
-- Nutze current_cast für Regrowth cleanup
if this.current_cast == REGROWTH then
this.regrowth_timer = nil
this.regrowth_start = nil
this.regrowth_target_next = nil
this.regrowth_start_next = nil
end
-- Cleanup
this.current_cast = nil
this.current_cast_target = nil
elseif event == "SPELL_FAILED_SELF" then
-- Nampower SPELL_FAILED_SELF: More reliable than vanilla SPELLCAST_FAILED
-- Same cleanup as SPELLCAST_FAILED
if libpredict.sender.healing then
libpredict:HealStop(player)
-- DEBUG: Log when sending HealStop
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cffff00ff[libpredict TX]|r Sending HealStop to group (SPELL_FAILED_SELF)")
end
libpredict.sender:SendHealCommMsg("Healstop")
libpredict.sender.healing = nil
elseif libpredict.sender.resurrecting then
local target = this.current_cast_target or senttarget or spell_queue[3]
libpredict:RessStop(player)
libpredict.sender:SendHealCommMsg("Resurrection/stop/")
libpredict.sender:SendResCommMsg("RESNO " .. target)
libpredict.sender.resurrecting = nil
end
-- Regrowth cleanup
if this.current_cast == REGROWTH then
this.regrowth_timer = nil
this.regrowth_start = nil
this.regrowth_target_next = nil
this.regrowth_start_next = nil
end
-- Cleanup
this.current_cast = nil
this.current_cast_target = nil
elseif event == "SPELLCAST_DELAYED" then
if libpredict.sender.healing then
libpredict:HealDelay(player, arg1)
libpredict.sender:SendHealCommMsg("Healdelay/" .. arg1 .. "/")
end
elseif strfind(event, "SPELLCAST_STOP", 1) then
if strfind(event, "UNIT_", 1) and arg1 ~= "player" then return end
elseif event == "SPELLCAST_STOP" then
libpredict:HealStop(player)
if pfUI.client < 20000 then -- vanilla
if spell_queue[1] == REJUVENATION then
libpredict:Hot(player, spell_queue[3], "Reju", rejuvDuration)
libpredict.sender:SendHealCommMsg("Reju/"..spell_queue[3].."/"..rejuvDuration.."/")
elseif spell_queue[1] == RENEW then
libpredict:Hot(player, spell_queue[3], "Renew", renewDuration)
libpredict.sender:SendHealCommMsg("Renew/"..spell_queue[3].."/"..renewDuration.."/")
elseif spell_queue[1] == REGROWTH then
this.regrowth_timer = GetTime() + 0.1
-- Nur Regrowth wird hier verarbeitet (hat Cast-Zeit)
-- Nutze this.current_cast (wird bei SPELLCAST_START gesetzt, nicht von Instant-Hooks überschrieben)
if this.current_cast == REGROWTH then
local now = pfUI.uf.now or GetTime()
if this.regrowth_timer then
-- Bereits ein Regrowth aktiv, speichere für den nächsten
this.regrowth_start_next = now
else
this.regrowth_start = now
end
else -- tbc
--todo
this.regrowth_timer = now + 0.1
end
-- Cleanup
this.current_cast = nil
this.current_cast_target = nil
end
end)
function libpredict:GetHotDuration(unit, spell)
if unit == UNKNOWNOBJECT or unit == UNKOWNBEING then return end
-- NEW: Try libdebuff first (Nampower AURA_CAST events)
if pfUI.api.libdebuff and pfUI.api.libdebuff.GetBestAuraCast then
local _, guid = UnitExists(unit) -- FIX: Get GUID, not exists boolean!
if guid then
-- Get the best (highest rank) aura cast for this spell
local spellName = spell
-- Map short spell codes to full names
if spell == "Reju" then
spellName = REJUVENATION
elseif spell == "Regr" then
spellName = REGROWTH
elseif spell == "Renew" then
spellName = RENEW
end
local start, duration, timeleft, rank, casterGuid = pfUI.api.libdebuff:GetBestAuraCast(guid, spellName)
if start and duration and timeleft then
-- SUCCESS: libdebuff has accurate server-side data!
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[GetHotDuration]|r %s on %s via libdebuff: dur=%.1fs timeleft=%.1fs rank=%d",
spell, unit, duration, timeleft, rank or 0))
end
return start, duration, timeleft
end
end
end
-- FALLBACK: Use old prediction system (for non-Nampower clients or no AURA_CAST data)
local start, duration, timeleft
local unitdata = hots[UnitName(unit)]
if unitdata and unitdata[spell] and (unitdata[spell].start + unitdata[spell].duration) > GetTime() - 1 then
start = unitdata[spell].start
duration = unitdata[spell].duration
timeleft = (start + duration) - GetTime()
local now = pfUI.uf.now or GetTime()
local unitName = UnitName(unit)
local unitdata = hots[unitName]
if unitdata and unitdata[spell] then
local spellData = unitdata[spell]
if spellData.start and spellData.duration then
local endTime = spellData.start + spellData.duration
if endTime > now - 1 then
start = spellData.start
duration = spellData.duration
timeleft = endTime - now
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cffff9900[GetHotDuration]|r %s on %s via prediction: dur=%.1fs timeleft=%.1fs",
spell, unit, duration, timeleft))
end
end
end
end
return start, duration, timeleft
end
pfUI.api.libpredict = libpredict
-- Debug command: /hotdebug - Show HoT tracking status
_G.SLASH_HOTDEBUG1 = "/hotdebug"
_G.SlashCmdList.HOTDEBUG = function()
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff========================================|r")
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[HoT Tracking Debug]|r")
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff========================================|r")
-- Check libdebuff availability
local libdebuff_now = (pfUI.api.libdebuff and pfUI.api.libdebuff.GetBestAuraCast) and true or false
if libdebuff_now then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[PRIMARY]|r libdebuff integration: ACTIVE")
DEFAULT_CHAT_FRAME:AddMessage(" Using AURA_CAST events for server-side tracking")
DEFAULT_CHAT_FRAME:AddMessage(" Rank protection: ENABLED")
else
DEFAULT_CHAT_FRAME:AddMessage("|cffff9900[PRIMARY]|r libdebuff integration: NOT AVAILABLE")
DEFAULT_CHAT_FRAME:AddMessage(" Reason: Nampower not enabled or libdebuff outdated")
end
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[FALLBACK]|r Legacy prediction system: ACTIVE")
DEFAULT_CHAT_FRAME:AddMessage(" Using UNIT_CASTEVENT + HealComm messages")
-- Show active HoTs in tracking
local hotCount = 0
for target, spells in pairs(hots) do
for spell, data in pairs(spells) do
hotCount = hotCount + 1
end
end
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ffff[TRACKED]|r %d HoTs in legacy system", hotCount))
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff========================================|r")
DEFAULT_CHAT_FRAME:AddMessage("Tip: /libpredict.debug = true for verbose logging")
end
pfUI.api.libpredict = libpredict
+48 -13
View File
@@ -1,6 +1,8 @@
-- load pfUI environment
setfenv(1, pfUI:GetEnvironment())
local superwow_active = HasSuperWoW()
--[[ librange ]]--
-- A pfUI library that detects and caches distance to units.
--
@@ -39,26 +41,29 @@ local spells = {
},
}
-- use native IsSpellInRange checker for tbc and skip
-- the whole targeting approach that is required for vanilla
if pfUI.expansion == "tbc" then
local spell
-- Use Nampower's IsSpellInRange if available (vanilla only)
-- This provides more accurate range checking without needing to find spell slots
local nampower_spell
if GetNampowerVersion then
librange:RegisterEvent("LEARNED_SPELL_IN_TAB")
librange:RegisterEvent("PLAYER_ENTERING_WORLD")
librange:SetScript("OnEvent", function()
-- abort on non healing classes
if not spells[class] then return end
nampower_spell = nil
for i = 1, GetNumSpellTabs() do
local _, _, offset, num = GetSpellTabInfo(i)
for id = offset + 1, offset + num do
local name, rank = GetSpellName(id, BOOKTYPE_SPELL)
local texture = GetSpellTexture(name)
local texture = GetSpellTexture(id, BOOKTYPE_SPELL)
for _, tex in pairs(spells[class]) do
if tex == texture then
spell = name
return
if texture then
for _, tex in pairs(spells[class]) do
if tex == texture then
nampower_spell = name
end
end
end
end
@@ -66,8 +71,12 @@ if pfUI.expansion == "tbc" then
end)
function librange:UnitInSpellRange(unit)
if not spell then return nil end
return IsSpellInRange(spell, unit) == 1 and true or nil
if not nampower_spell then return nil end
-- Nampower's IsSpellInRange returns 1 if in range, 0 if not, -1 if invalid
local result = IsSpellInRange(nampower_spell, unit)
if result == 1 then return 1
elseif result == 0 then return nil
else return nil end
end
-- add librange to pfUI API
@@ -114,12 +123,25 @@ combo:SetScript("OnEvent", function()
hascombopoints = GetComboPoints() > 0
end)
-- Flag to prevent UnitXP calls during logout (crash prevention)
local librange_isLoggingOut = false
librange:Hide()
librange:RegisterEvent("ACTIONBAR_SLOT_CHANGED")
librange:RegisterEvent("PLAYER_ENTERING_WORLD")
librange:RegisterEvent("PLAYER_ENTER_COMBAT")
librange:RegisterEvent("PLAYER_LEAVE_COMBAT")
librange:RegisterEvent("PLAYER_LOGOUT")
librange:RegisterEvent("PLAYER_LEAVING_WORLD")
librange:SetScript("OnEvent", function()
-- Handle logout to prevent UnitXP crashes during shutdown
if event == "PLAYER_LOGOUT" or event == "PLAYER_LEAVING_WORLD" then
librange_isLoggingOut = true
this:SetScript("OnUpdate", nil) -- Stop OnUpdate completely
this:Hide()
return
end
-- disable range checking activities
if pfUI_config.unitframes.rangecheck == "0" or not spells[class] then
this:Hide()
@@ -147,6 +169,9 @@ local target_event = TargetFrame_OnEvent
local target_nop = function() return end
librange:SetScript("OnUpdate", function()
-- Prevent UnitXP calls during logout (crash prevention)
if librange_isLoggingOut then return end
if ( this.tick or 1) > GetTime() then
return
else
@@ -161,7 +186,17 @@ librange:SetScript("OnUpdate", function()
if this.id <= numunits and librange.slot then
local unit = units[this.id]
if not UnitIsUnit("target", unit) then
-- try to read distance via superwow first
-- Try UnitXP_SP3 first (most accurate distance measurement)
local unitxp_success, unitxp_distance = pcall(function()
return UnitXP("distanceBetween", "player", unit)
end)
if unitxp_success and unitxp_distance then
unitdata[unit] = unitxp_distance < 45 and 1 or 0
this.id = this.id + 1
return
end
-- try to read distance via superwow second
if superwow_active then
local x1, y1, z1 = UnitPosition("player")
local x2, y2, z2 = UnitPosition(unit)
@@ -282,4 +317,4 @@ function librange:UnitInSpellRange(unit)
end
-- add librange to pfUI API
pfUI.api.librange = librange
pfUI.api.librange = librange
+172
View File
@@ -0,0 +1,172 @@
-- load pfUI environment
setfenv(1, pfUI:GetEnvironment())
-- return instantly when another libthrottle is already active
if pfUI.api.libthrottle then return end
-- Create libthrottle namespace
local libthrottle = CreateFrame("Frame", "pfLibThrottle")
pfUI.api.libthrottle = libthrottle
-- Preset definitions (FPS -> seconds)
libthrottle.presets = {
["very_slow"] = { fps = 2, delay = 0.5 },
["slow"] = { fps = 5, delay = 0.2 },
["normal"] = { fps = 10, delay = 0.1 },
["fast"] = { fps = 20, delay = 0.05 },
["very_fast"] = { fps = 30, delay = 0.033 },
["fastest"] = { fps = 50, delay = 0.02 },
}
-- Localized preset names
libthrottle.presetNames = {
["very_slow"] = "Very Slow (2 FPS)",
["slow"] = "Slow (5 FPS)",
["normal"] = "Normal (10 FPS)",
["fast"] = "Fast (20 FPS)",
["very_fast"] = "Very Fast (30 FPS)",
["fastest"] = "Fastest (50 FPS)",
["custom"] = "Custom",
}
-- Default throttle categories
libthrottle.defaults = {
nameplates = "custom",
nameplates_target = "custom",
nameplates_mass = "custom",
tooltip_cursor = "custom",
chat_tab = "custom",
}
-- Convert FPS to throttle delay in seconds
function libthrottle:FpsToDelay(fps)
if not fps or fps <= 0 then return 0.1 end
return 1 / fps
end
-- Convert delay to FPS
function libthrottle:DelayToFps(delay)
if not delay or delay <= 0 then return 10 end
return math.floor(1 / delay)
end
-- Get throttle delay for a category
-- Returns: delay in seconds
function libthrottle:Get(category)
local configValue = _G.pfUI_throttle and _G.pfUI_throttle[category]
if not configValue then
configValue = self.defaults[category] or "normal"
end
-- Check if it's a preset name
local preset = self.presets[configValue]
if preset then
return preset.delay
end
-- If it's "custom", read from the _custom field
if configValue == "custom" then
local customFps = tonumber(_G.pfUI_throttle[category .. "_custom"])
if customFps then
return self:FpsToDelay(customFps)
end
end
-- Fallback to normal preset
return self.presets["normal"].delay
end
-- Get FPS value for a category (for display purposes)
function libthrottle:GetFps(category)
local delay = self:Get(category)
return self:DelayToFps(delay)
end
-- Get preset name for a category
function libthrottle:GetPreset(category)
local configValue = _G.pfUI_throttle and _G.pfUI_throttle[category]
if not configValue then
return self.defaults[category] or "normal"
end
-- Check if it's a known preset
if self.presets[configValue] then
return configValue
end
-- Must be a custom value
return "custom"
end
-- Check if a category is using custom FPS
function libthrottle:IsCustom(category)
return self:GetPreset(category) == "custom"
end
-- Set throttle for a category
function libthrottle:Set(category, value)
if not _G.pfUI_throttle then _G.pfUI_throttle = {} end
-- Validate preset
if type(value) == "string" and self.presets[value] then
_G.pfUI_throttle[category] = value
return true
end
-- If it's "custom", keep it
if value == "custom" then
_G.pfUI_throttle[category] = value
return true
end
return false
end
-- Reset a category to its default value
function libthrottle:ResetToDefault(category)
local default = self.defaults[category]
if default then
if not _G.pfUI_throttle then _G.pfUI_throttle = {} end
_G.pfUI_throttle[category] = default
return true
end
return false
end
-- Reset all categories to defaults
function libthrottle:ResetAllToDefaults()
if not _G.pfUI_throttle then _G.pfUI_throttle = {} end
for category, default in pairs(self.defaults) do
_G.pfUI_throttle[category] = default
end
end
-- Initialize - set defaults if config doesn't exist
libthrottle:SetScript("OnEvent", function()
if event == "PLAYER_ENTERING_WORLD" then
if not _G.pfUI_throttle then
_G.pfUI_throttle = {}
end
-- Set defaults for any missing categories
for category, default in pairs(libthrottle.defaults) do
if not _G.pfUI_throttle[category] then
_G.pfUI_throttle[category] = default
end
end
-- Set defaults for custom fields if missing
if not _G.pfUI_throttle.nameplates_target_custom then _G.pfUI_throttle.nameplates_target_custom = "50" end
if not _G.pfUI_throttle.nameplates_custom then _G.pfUI_throttle.nameplates_custom = "10" end
if not _G.pfUI_throttle.nameplates_mass_custom then _G.pfUI_throttle.nameplates_mass_custom = "7" end
if not _G.pfUI_throttle.tooltip_cursor_custom then _G.pfUI_throttle.tooltip_cursor_custom = "10" end
if not _G.pfUI_throttle.chat_tab_custom then _G.pfUI_throttle.chat_tab_custom = "10" end
this:UnregisterEvent("PLAYER_ENTERING_WORLD")
end
end)
libthrottle:RegisterEvent("PLAYER_ENTERING_WORLD")
-- Export to pfUI namespace for easier access
pfUI.throttle = libthrottle
+4
View File
@@ -131,6 +131,8 @@ libunitscan:SetScript("OnEvent", function()
if UnitIsPlayer(scan) then
_, class = UnitClass(scan)
level = UnitLevel(scan)
-- UnitLevel returns -1 for unknown levels, don't overwrite known values
level = level > 0 and level or nil
name = UnitName(scan)
guild = GetGuildInfo(scan)
AddData("players", name, class, level, nil, guild)
@@ -138,6 +140,8 @@ libunitscan:SetScript("OnEvent", function()
_, class = UnitClass(scan)
elite = UnitClassification(scan)
level = UnitLevel(scan)
-- UnitLevel returns -1 for unknown levels, don't overwrite known values
level = level > 0 and level or nil
name = UnitName(scan)
AddData("mobs", name, class, level, elite)
end
+114 -51
View File
@@ -1,4 +1,4 @@
pfUI:RegisterModule("actionbar", "vanilla:tbc", function ()
pfUI:RegisterModule("actionbar", "vanilla", function ()
local _, class = UnitClass("player")
local color = RAID_CLASS_COLORS[class]
local cr, cg, cb = color.r , color.g, color.b
@@ -668,7 +668,10 @@ pfUI:RegisterModule("actionbar", "vanilla:tbc", function ()
start, duration, enable = GetActionCooldown(button.id)
end
CooldownFrame_SetTimer(button.cd, start, duration, enable)
-- Nil-protect: GetActionCooldown can return nil during macro parsing/indexing
if start and duration then
CooldownFrame_SetTimer(button.cd, start, duration, enable or 1)
end
end
local _, active
@@ -755,9 +758,14 @@ pfUI:RegisterModule("actionbar", "vanilla:tbc", function ()
end
local self, button, unlock
-- Main update loop with throttle for performance optimization
local function BarsUpdate(self)
self = self or this
-- Throttle for performance
if (this.tick_main or 0) > GetTime() then return end
this.tick_main = GetTime() + 0.025
-- update buttons whenever a button drag is assumed
AssumeButtonDrag()
@@ -912,24 +920,40 @@ pfUI:RegisterModule("actionbar", "vanilla:tbc", function ()
end
local cat, stealth
local function IsCatStealth()
local inCatForm = nil -- cached from buff scan
local prowlActive = nil -- tracks if prowl is active
-- Full scan for cat form and prowl (only on login/reload)
local function FullScan()
if class ~= "DRUID" then return nil end
cat, stealth = nil, nil
local foundCat, foundStealth = nil, nil
for i = 0, 31 do
local texture = GetPlayerBuffTexture(i)
if not texture then break end
-- catform icon detected
if strfind(texture, "Ability_Druid_CatForm") then
if stealth then return true end
cat = true
foundCat = true
end
-- stealth icon detected
if strfind(texture, "Ability_Ambush") then
if cat then return true end
stealth = true
foundStealth = true
end
end
inCatForm = foundCat
prowlActive = foundCat and foundStealth
return prowlActive
end
-- Quick scan only for prowl (when we know we're in cat form)
local function HasProwlBuff()
for i = 0, 31 do
local texture = GetPlayerBuffTexture(i)
if not texture then break end
if strfind(texture, "Ability_Ambush") then
return true
end
end
return nil
@@ -956,7 +980,76 @@ pfUI:RegisterModule("actionbar", "vanilla:tbc", function ()
end
-- setup page switch frame
local prowling = nil
local pageswitch = CreateFrame("Frame", "pfActionBarPageSwitch", UIParent)
pageswitch:RegisterEvent("PLAYER_AURAS_CHANGED")
pageswitch:RegisterEvent("PLAYER_ENTERING_WORLD")
pageswitch:RegisterEvent("UNIT_CASTEVENT")
pageswitch:RegisterEvent("PLAYER_LOGOUT")
pageswitch:SetScript("OnEvent", function()
-- Handle shutdown to prevent crash 132
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
this:SetScript("OnUpdate", nil)
return
end
if class ~= "DRUID" then return end
-- On login/reload: full scan
if event == "PLAYER_ENTERING_WORLD" then
prowling = FullScan()
return
end
-- UNIT_CASTEVENT: detect Prowl cast instantly
-- Prowl Spell IDs: 5215 (Rank 1), 6783 (Rank 2), 9913 (Rank 3)
if event == "UNIT_CASTEVENT" then
local guid, target, cEvent, spellId = arg1, arg2, arg3, arg4
local _, playerGuid = UnitExists("player")
if guid == playerGuid and cEvent == "CAST" then
if spellId == 5215 or spellId == 6783 or spellId == 9913 then
-- Prowl cast detected
inCatForm = true
prowlActive = true
prowling = true
elseif spellId == 768 then
-- Cat Form cast (Spell ID 768)
inCatForm = true
end
end
return
end
-- PLAYER_AURAS_CHANGED: smart scanning
if event == "PLAYER_AURAS_CHANGED" then
if prowlActive then
-- We were prowling, check if still prowling
if HasProwlBuff() then
prowling = true
else
-- Prowl ended
prowlActive = nil
prowling = nil
-- Also check if still in cat form
inCatForm = nil
for i = 0, 31 do
local texture = GetPlayerBuffTexture(i)
if not texture then break end
if strfind(texture, "Ability_Druid_CatForm") then
inCatForm = true
break
end
end
end
elseif not inCatForm then
-- Not in cat form, do a full scan (might have just shifted)
prowling = FullScan()
end
-- If inCatForm but not prowlActive, no scan needed (wait for UNIT_CASTEVENT)
end
end)
pageswitch:SetScript("OnUpdate", function()
-- switch actionbar page depending on meta key that is pressed
if C.bars.pagemastershift == "1" and IsShiftKeyDown() then
@@ -974,10 +1067,9 @@ pfUI:RegisterModule("actionbar", "vanilla:tbc", function ()
-- switch actionbar page if druid stealth is detected
if C.bars.druidstealth == "1" then
local stealth = IsCatStealth()
if stealth and _G.CURRENT_ACTIONBAR_PAGE == 1 then
if prowling and _G.CURRENT_ACTIONBAR_PAGE == 1 then
SwitchBar(prowl)
elseif not stealth and _G.CURRENT_ACTIONBAR_PAGE == 8 then
elseif not prowling and _G.CURRENT_ACTIONBAR_PAGE == 8 then
SwitchBar(default)
end
end
@@ -1109,39 +1201,6 @@ pfUI:RegisterModule("actionbar", "vanilla:tbc", function ()
buttoncache[id] = f
end
-- set required attributes for regular tbc buttons
if pfUI.client > 11200 then
if bar == 11 then
f:SetAttribute("type", "spell")
f:SetAttribute('spell', select(2, GetShapeshiftFormInfo(button)))
elseif bar == 12 then
f:SetAttribute("type1", "pet")
f:SetAttribute("action1", button)
f:SetAttribute("type2", "macro")
f:SetAttribute("macrotext2", "/click PetActionButton".. button .. " RightButton")
else
bars[bar]:SetAttribute("addchild", f)
f:SetAttribute("type", "action")
f:SetAttribute("action", id)
f:SetAttribute("checkselfcast", true)
f:SetAttribute("useparent-unit", true)
f:SetAttribute("useparent-statebutton", true)
for state = 0, 11 do -- add custom states
local action = ((state == 0 and bar or state)-1)*12+button
f:SetAttribute(string.format("*type-S%d", state), "action")
f:SetAttribute(string.format("*type-S%dRight", state), "action")
f:SetAttribute(string.format("*action-S%d", state), action)
f:SetAttribute(string.format("*action-S%dRight", state), action)
if C.bars.rightself == "1" then
f:SetAttribute(string.format("*unit-S%dRight", state), "player")
else
f:SetAttribute(string.format("*unit-S%dRight", state), nil)
end
end
end
end
-- set keydown option
if C.bars.keydown == "1" then
f:RegisterForClicks("LeftButtonDown", "RightButtonDown")
@@ -1183,8 +1242,8 @@ pfUI:RegisterModule("actionbar", "vanilla:tbc", function ()
f.count:SetJustifyH("RIGHT")
f.count:SetJustifyV("BOTTOM")
-- macro spell scan
if C.bars.macroscan == "0" then
-- macro spell scan (disabled when macro addons are loaded)
if C.bars.macroscan == "0" or pfUI:MacroAddonsLoaded() then
f.scanmacro, f.spellslot, f.booktype = nil, nil, nil
else
f.scanmacro = true
@@ -1653,9 +1712,13 @@ pfUI:RegisterModule("actionbar", "vanilla:tbc", function ()
end
end)
-- limit events to one per second and smoothen action scanning
-- Reagent counter update with throttle for performance optimization
reagentcounter:SetScript("OnUpdate", function()
-- scan one action slot per frame
-- Throttle entire function to 10 FPS for smooth scanning
if (this.tick_update or 0) > GetTime() then return end
this.tick_update = GetTime() + 0.1
-- scan one action slot per update
if this.scan and this.scan <= 120 then
UpdateSlot(this.scan)
this.scan = this.scan + 1
+60
View File
@@ -0,0 +1,60 @@
pfUI:RegisterModule("bgscore", "vanilla", function ()
local bgframe = WorldStateAlwaysUpFrame
if not bgframe then
bgframe = CreateFrame("Frame", "WorldStateAlwaysUpFrame", UIParent)
bgframe:SetWidth(200)
bgframe:SetHeight(25)
bgframe:SetPoint("TOP", UIParent, "TOP", 0, -100)
end
local mover = CreateFrame("Frame", "pfUIBGScoreMover", UIParent)
mover:SetWidth(220)
mover:SetHeight(30)
mover:SetPoint("TOP", UIParent, "TOP", 0, -100)
mover:SetFrameStrata("DIALOG")
mover:SetMovable(true)
mover:EnableMouse(true)
mover:RegisterForDrag("LeftButton")
mover:SetScript("OnDragStart", function() mover:StartMoving() end)
mover:SetScript("OnDragStop", function()
mover:StopMovingOrSizing()
local x = mover:GetLeft()
local y = mover:GetTop()
pfUI_config = pfUI_config or {}
pfUI_config.positions = pfUI_config.positions or {}
pfUI_config.positions["WorldStateAlwaysUpFrame"] = { x = x, y = y }
bgframe:ClearAllPoints()
bgframe:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", x, y)
DEFAULT_CHAT_FRAME:AddMessage("|cff33ffccBG Score Frame|r position saved.")
end)
mover:Hide()
pfUI.api.CreateBackdrop(mover, nil, nil, .8)
-- Title label
local title = mover:CreateFontString(nil, "OVERLAY")
title:SetFont("Fonts\\FRIZQT__.TTF", 14, "OUTLINE")
title:SetText("Battleground Frames")
title:SetPoint("TOP", mover, "TOP", 0, -2)
-- BG score preview text
local bgscore = mover:CreateFontString(nil, "OVERLAY")
bgscore:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE")
bgscore:SetText("|cff3399ffAlliance: 123|r | |cffff4444Horde: 456|r")
bgscore:SetPoint("BOTTOM", mover, "BOTTOM", 0, 2)
mover.label = "BG Score"
pfUI.unlock.frames = pfUI.unlock.frames or {}
table.insert(pfUI.unlock.frames, mover)
local pos = pfUI_config and pfUI_config.positions and pfUI_config.positions["WorldStateAlwaysUpFrame"]
if pos then
bgframe:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", pos.x, pos.y)
mover:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", pos.x, pos.y)
end
local origShow = pfUI.unlock.Show
local origHide = pfUI.unlock.Hide
pfUI.unlock.Show = function(self) origShow(self); mover:Show() end
pfUI.unlock.Hide = function(self) origHide(self); mover:Hide(); bgframe:Show() end
end)
+116 -26
View File
@@ -76,8 +76,55 @@ pfUI:RegisterModule("buff", "vanilla:tbc", function ()
buff.backdrop:SetBackdropBorderColor(br,bg,bb,ba)
end
else
buff:Hide()
return
-- Fallback: try UnitBuff/UnitDebuff API which may be more reliable in some cases
local fallbackTexture, fallbackStacks, fallbackDispelType, fallbackSpellId
local maxSlots = buff.btype == "HELPFUL" and 32 or 16
if buff.id >= 1 and buff.id <= maxSlots then
if buff.btype == "HELPFUL" and C.buffs.buffs == "1" then
for i = 1, maxSlots do
local tex, stacks, dtype, spellId = UnitBuff("player", i)
if tex and i == buff.id then
fallbackTexture, fallbackStacks, fallbackDispelType, fallbackSpellId = tex, stacks, dtype, spellId
break
end
if not tex then break end
end
elseif buff.btype == "HARMFUL" and C.buffs.debuffs == "1" then
for i = 1, maxSlots do
local tex, stacks, dtype, spellId = UnitDebuff("player", i)
if tex and i == buff.id then
fallbackTexture, fallbackStacks, fallbackDispelType, fallbackSpellId = tex, stacks, dtype, spellId
break
end
if not tex then break end
end
end
end
if fallbackTexture then
buff.mode = buff.btype
buff.fallbackSpellId = fallbackSpellId
buff.texture:SetTexture(fallbackTexture)
if buff.btype == "HARMFUL" then
if fallbackDispelType == "Magic" then
buff.backdrop:SetBackdropBorderColor(0,1,1,1)
elseif fallbackDispelType == "Poison" then
buff.backdrop:SetBackdropBorderColor(0,1,0,1)
elseif fallbackDispelType == "Curse" then
buff.backdrop:SetBackdropBorderColor(1,0,1,1)
elseif fallbackDispelType == "Disease" then
buff.backdrop:SetBackdropBorderColor(1,1,0,1)
else
buff.backdrop:SetBackdropBorderColor(1,0,0,1)
end
else
buff.backdrop:SetBackdropBorderColor(br,bg,bb,ba)
end
else
buff:Hide()
return
end
end
buff:Show()
@@ -119,30 +166,8 @@ pfUI:RegisterModule("buff", "vanilla:tbc", function ()
buff.btype = btype
buff.gid = i
buff:SetScript("OnUpdate", function()
if not this.next then this.next = GetTime() + .1 end
if this.next > GetTime() then return end
this.next = GetTime() + .1
local timeleft = 0
local stacks = 0
if this.mode == this.btype then
timeleft = GetPlayerBuffTimeLeft(this.bid, this.btype)
stacks = GetPlayerBuffApplications(this.bid, this.btype)
elseif this.mode == "MAINHAND" then
local _, mhtime, mhcharge = GetWeaponEnchantInfo()
timeleft = mhtime/1000
stacks = mhcharge
elseif this.mode == "OFFHAND" then
local _, _, _, _, ohtime, ohcharge = GetWeaponEnchantInfo()
timeleft = ohtime/1000
stacks = ohcharge
end
this.timer:SetText(timeleft > 0 and GetColoredTimeString(timeleft) or "")
this.stacks:SetText(stacks > 1 and stacks or "")
end)
-- PERF: OnUpdate moved to consolidated parent frame handler (see pfUI.buff:SetScript("OnUpdate"))
-- Individual buff frames no longer have their own OnUpdate
buff:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
@@ -251,6 +276,71 @@ pfUI:RegisterModule("buff", "vanilla:tbc", function ()
end
end)
-- PERF: Consolidated OnUpdate handler for all buff timers
-- This replaces 50 individual OnUpdate handlers with a single one
pfUI.buff:SetScript("OnUpdate", function()
local now = GetTime()
if not this.nextUpdate then this.nextUpdate = now + 0.1 end
if this.nextUpdate > now then return end
this.nextUpdate = now + 0.1
-- Cache weapon enchant info once per update cycle
local mh, mhtime, mhcharge, oh, ohtime, ohcharge = GetWeaponEnchantInfo()
-- Update all visible buff buttons
local buttons = pfUI.buff.buffs.buttons
for i = 1, 32 do
local buff = buttons[i]
if buff:IsShown() then
local timeleft, stacks = 0, 0
if buff.mode == buff.btype then
timeleft = GetPlayerBuffTimeLeft(buff.bid, buff.btype)
stacks = GetPlayerBuffApplications(buff.bid, buff.btype)
elseif buff.mode == "MAINHAND" then
timeleft = mhtime and mhtime / 1000 or 0
stacks = mhcharge or 0
elseif buff.mode == "OFFHAND" then
timeleft = ohtime and ohtime / 1000 or 0
stacks = ohcharge or 0
end
buff.timer:SetText(timeleft > 0 and GetColoredTimeString(timeleft) or "")
buff.stacks:SetText(stacks > 1 and stacks or "")
end
end
-- Update all visible debuff buttons
buttons = pfUI.buff.debuffs.buttons
for i = 1, 16 do
local buff = buttons[i]
if buff:IsShown() then
local timeleft = GetPlayerBuffTimeLeft(buff.bid, buff.btype)
local stacks = GetPlayerBuffApplications(buff.bid, buff.btype)
buff.timer:SetText(timeleft > 0 and GetColoredTimeString(timeleft) or "")
buff.stacks:SetText(stacks > 1 and stacks or "")
end
end
-- Update weapon buff buttons if separate
if C.buffs.separateweapons == "1" then
buttons = pfUI.buff.wepbuffs.buttons
for i = 1, 2 do
local buff = buttons[i]
if buff:IsShown() then
local timeleft, stacks = 0, 0
if buff.mode == "MAINHAND" then
timeleft = mhtime and mhtime / 1000 or 0
stacks = mhcharge or 0
elseif buff.mode == "OFFHAND" then
timeleft = ohtime and ohtime / 1000 or 0
stacks = ohcharge or 0
end
buff.timer:SetText(timeleft > 0 and GetColoredTimeString(timeleft) or "")
buff.stacks:SetText(stacks > 1 and stacks or "")
end
end
end
end)
-- Weapon Buffs
pfUI.buff.wepbuffs = CreateFrame("Frame", "pfWepBuffFrame", UIParent)
pfUI.buff.wepbuffs.count = 0
+25 -2
View File
@@ -125,7 +125,23 @@ pfUI:RegisterModule("buffwatch", "vanilla:tbc", function ()
if this.unit == "player" then
GameTooltip:SetPlayerBuff(GetPlayerBuff(PLAYER_BUFF_START_ID+this.id,this.type))
elseif this.type == "HARMFUL" then
GameTooltip:SetUnitDebuff(this.unit, this.id)
-- For "only own debuffs" mode: find the REAL slot by matching spell name AND caster
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
for gameSlot = 1, 16 do
local gameName, _, _, _, _, _, _, gameCaster = libdebuff:UnitDebuff(this.unit, gameSlot)
if gameName == ownDebuffName and gameCaster == "player" then
GameTooltip:SetUnitDebuff(this.unit, gameSlot)
break
end
end
end
else
GameTooltip:SetUnitDebuff(this.unit, this.id)
end
elseif this.type == "HELPFUL" then
GameTooltip:SetUnitBuff(this.unit, this.id)
end
@@ -261,7 +277,14 @@ pfUI:RegisterModule("buffwatch", "vanilla:tbc", function ()
and data[3] and data[3] ~= "" -- buff has a name
and data[4] and data[4] ~= "" -- buff has a texture
then
local uuid = data[4] .. data[3] -- we use that to cache some values for buffs
-- For player: no slot in uuid (slots shift when other buffs expire)
-- For target: include slot (multiple players can have same debuff, slot identifies who)
local uuid
if frame.unit == "player" then
uuid = data[4] .. data[3] -- texture + name only
else
uuid = data[4] .. data[3] .. data[2] -- texture + name + slot
end
-- update bar data
frame.bars[bar] = frame.bars[bar] or CreateStatusBar(bar, frame)
+88 -10
View File
@@ -1,9 +1,22 @@
pfUI:RegisterModule("castbar", "vanilla:tbc", function ()
pfUI:RegisterModule("castbar", "vanilla", function ()
local superwow_active = HasSuperWoW()
local font = C.castbar.use_unitfonts == "1" and pfUI.font_unit or pfUI.font_default
local font_size = C.castbar.use_unitfonts == "1" and C.global.font_unit_size or C.global.font_size
local rawborder, default_border = GetBorderSize("unitframes")
local cbtexture = pfUI.media[C.appearance.castbar.texture]
-- Helper function for castbar timer formatting
local function FormatCastbarTime(value)
if C.unitframes.castbardecimals == "1" then
-- 1 decimal, always floor
return string.format("%.1f", floor(value * 10) / 10)
else
-- 2 decimals (default)
return string.format("%.2f", value)
end
end
local function CreateCastbar(name, parent, unitstr, unitname)
local cb = CreateFrame("Frame", name, parent or UIParent)
@@ -64,7 +77,12 @@ pfUI:RegisterModule("castbar", "vanilla:tbc", 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
cb:SetScript("OnUpdate", function()
-- Throttle for performance
if (this.tick or 0) > GetTime() then return end
this.tick = GetTime() + 0.020 -- ~60 FPS for smooth castbar
if this.drag and this.drag:IsShown() then
this:SetAlpha(1)
return
@@ -86,14 +104,46 @@ pfUI:RegisterModule("castbar", "vanilla:tbc", function ()
local query = this.unitstr ~= "" and this.unitstr or this.unitname
if not query then return end
-- transform all non player unitstrings to unit guids
if superwow_active and this.unitstr and not UnitIsUnit(this.unitstr, 'player') then
-- 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
end
-- Try libdebuff_casts first for GUID-based units (works with Turtle GUID + Nampower events)
local cast, nameSubtext, text, texture, startTime, endTime
if focusGuid and pfUI.libdebuff_casts and pfUI.libdebuff_casts[focusGuid] then
local castData = pfUI.libdebuff_casts[focusGuid]
if castData.event == "START" and castData.endTime and castData.endTime > GetTime() then
cast = castData.spellName
texture = castData.icon
startTime = castData.startTime * 1000 -- libdebuff uses seconds, castbar expects milliseconds
endTime = castData.endTime * 1000
nameSubtext = "" -- Rank info not available in libdebuff_casts
end
end
-- Fallback: transform unitstrings to unit guids when SuperWoW is active
-- SuperWoW stores cast data by GUID for all units INCLUDING player
-- BUT: For player casts, we need to use libcast data because it handles pushback correctly
local useLibcastForPlayer = this.unitstr == "player"
if not cast and superwow_active and this.unitstr and not useLibcastForPlayer then
local _, guid = UnitExists(this.unitstr)
query = guid or query
end
-- For player: use player name to query libcast.db directly
if not cast and useLibcastForPlayer then
query = UnitName("player")
end
local cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill = UnitCastingInfo(query)
if not cast then
-- Fallback: Try UnitCastingInfo if we haven't found cast data yet
if not cast and UnitCastingInfo then
cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill = UnitCastingInfo(query)
end
if not cast and UnitChannelInfo then
-- scan for channel spells if no cast was found
channel, nameSubtext, text, texture, startTime, endTime, isTradeSkill = UnitChannelInfo(query)
cast = channel
@@ -122,8 +172,35 @@ pfUI:RegisterModule("castbar", "vanilla:tbc", function ()
this.icon:Show()
this.icon:SetHeight(size)
this.icon:SetWidth(size)
this.icon.texture:SetTexture(texture)
-- 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 = UnitExists(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()
@@ -149,10 +226,10 @@ pfUI:RegisterModule("castbar", "vanilla:tbc", function ()
if this.showtimer then
if this.delay and this.delay > 0 then
local delay = "|cffffaaaa" .. (channel and "-" or "+") .. round(this.delay,1) .. " |r "
this.bar.right:SetText(delay .. string.format("%.1f",cur) .. " / " .. round(max,1))
local delay = "|cffffaaaa" .. (channel and "-" or "+") .. FormatCastbarTime(this.delay) .. " |r "
this.bar.right:SetText(delay .. FormatCastbarTime(cur) .. " / " .. FormatCastbarTime(max))
else
this.bar.right:SetText(string.format("%.1f",cur) .. " / " .. round(max,1))
this.bar.right:SetText(FormatCastbarTime(cur) .. " / " .. FormatCastbarTime(max))
end
end
@@ -162,6 +239,7 @@ pfUI:RegisterModule("castbar", "vanilla:tbc", function ()
this.bar:SetValue(100)
this.fadeout = 1
this.delay = 0
this.itemIconApplied = nil
end
end)
@@ -310,4 +388,4 @@ pfUI:RegisterModule("castbar", "vanilla:tbc", function ()
UpdateMovable(pfUI.castbar.focus)
end
end)
end)
+27
View File
@@ -455,6 +455,8 @@ pfUI:RegisterModule("chat", "vanilla:tbc", function ()
if C.chat.global.tabmouse == "1" then
pfUI.chat.mouseovertab = CreateFrame("Frame")
pfUI.chat.mouseovertab:SetScript("OnUpdate", function()
-- throttle
if ( this.tick or .1) > GetTime() then return else this.tick = GetTime() + pfUI.throttle:Get("chat_tab") end -- Default: Normal (10 FPS)
if pfUI.chat.hideLock then return end
@@ -725,6 +727,12 @@ pfUI:RegisterModule("chat", "vanilla:tbc", function ()
end
end)
local function GetPlayerLevel(name)
if not pfUI_playerDB then return nil end
if not pfUI_playerDB[name] then return nil end
return pfUI_playerDB[name].level
end
local function ScanWhoName(name)
-- abort if another query is ongoing
if who_query.pending then return end
@@ -783,6 +791,25 @@ pfUI:RegisterModule("chat", "vanilla:tbc", function ()
end
end
-- display player levels if available
if C.chat.text.playerlevel == "1" then
for name in gfind(text, "|Hplayer:(.-)|h") do
local real, _ = strsplit(":", name)
local level = GetPlayerLevel(real)
if level and level > 0 then
local levelcolor = rgbhex(GetDifficultyColor(level))
-- Add level after the player name, before the closing bracket
text = string.gsub(text, "(|Hplayer:" .. name .. "|h.-|h|r)" .. right,
"%1 " .. levelcolor .. level .. "|r" .. right)
elseif level and level <= 0 then
-- Show ?? for unknown levels (e.g. -1 from UnitLevel)
text = string.gsub(text, "(|Hplayer:" .. name .. "|h.-|h|r)" .. right,
"%1 |cffff0000??|r" .. right)
end
end
end
-- reduce channel name to number
if C.chat.text.channelnumonly == "1" then
local channel = string.gsub(text, ".*%[(.-)%]%s+(.*|Hplayer).+", "%1")
+1 -1
View File
@@ -144,4 +144,4 @@ pfUI:RegisterModule("cooldown", "vanilla:tbc", function ()
local methods = getmetatable(CreateFrame('Cooldown', nil, nil, 'CooldownFrameTemplate')).__index
hooksecurefunc(methods, 'SetCooldown', SetCooldown)
end
end)
end)
+20 -6
View File
@@ -7,6 +7,9 @@ pfUI:RegisterModule("energytick", "vanilla:tbc", function ()
energytick:RegisterEvent("UNIT_DISPLAYPOWER")
energytick:RegisterEvent("UNIT_ENERGY")
energytick:RegisterEvent("UNIT_MANA")
energytick:RegisterEvent("CHAT_MSG_SPELL_SELF_BUFF")
energytick:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_SELF_BUFFS")
energytick:SetScript("OnEvent", function()
if UnitPowerType("player") == 0 and C.unitframes.player.manatick == "1" then
this.mode = "MANA"
@@ -18,6 +21,14 @@ pfUI:RegisterModule("energytick", "vanilla:tbc", function ()
this:Hide()
end
-- Filter nur eigene Energy-Gewinne von Talents/Buffs
if event == "CHAT_MSG_SPELL_SELF_BUFF" or event == "CHAT_MSG_SPELL_PERIODIC_SELF_BUFFS" then
if string.find(arg1, "You gain") and string.find(arg1, "Energy from") then
this.ignoreNextGain = true
end
return
end
if event == "PLAYER_ENTERING_WORLD" then
this.lastMana = UnitMana("player")
end
@@ -38,13 +49,20 @@ pfUI:RegisterModule("energytick", "vanilla:tbc", function ()
this.badtick = diff
end
elseif this.mode == "ENERGY" and diff > 0 then
this.target = 2
if not this.ignoreNextGain then
this.target = 2
end
this.ignoreNextGain = false
end
this.lastMana = this.currentMana
end
end)
energytick:SetScript("OnUpdate", function()
-- Throttle for performance
if (this.tick or 0) > GetTime() then return end
this.tick = GetTime() + 0.020
if this.target then
this.start, this.max = GetTime(), this.target
this.target = nil
@@ -69,14 +87,10 @@ pfUI:RegisterModule("energytick", "vanilla:tbc", function ()
energytick.spark:SetWidth(C.unitframes.player.pheight + 5)
energytick.spark:SetBlendMode('ADD')
-- update spark size on player frame changes
local hookUpdateConfig = pfUI.uf.player.UpdateConfig
function pfUI.uf.player.UpdateConfig()
-- update spark sizes
energytick.spark:SetHeight(C.unitframes.player.pheight + 15)
energytick.spark:SetWidth(C.unitframes.player.pheight + 5)
-- run default unitframe update function
hookUpdateConfig(pfUI.uf.player)
end
end)
end)
+152 -13
View File
@@ -17,17 +17,95 @@ end)
-- register focus emulation commands for vanilla
if pfUI.client > 11200 then return end
-- Helper: set focus frame to a GUID
local function SetFocusByGUID(guid)
pfUI.uf.focus.unitname = nil
pfUI.uf.focus.label = guid
pfUI.uf.focus.id = ""
if pfUI.uf.focustarget then
pfUI.uf.focustarget.unitname = nil
pfUI.uf.focustarget.label = guid .. "target"
pfUI.uf.focustarget.id = ""
end
end
-- Helper: set focus frame by name (fallback, no Nampower)
local function SetFocusByName(name)
pfUI.uf.focus.unitname = strlower(name)
pfUI.uf.focus.label = nil
pfUI.uf.focus.id = nil
if pfUI.uf.focustarget then
pfUI.uf.focustarget.unitname = strlower(name) .. "target"
pfUI.uf.focustarget.label = nil
pfUI.uf.focustarget.id = nil
end
end
SLASH_PFFOCUS1, SLASH_PFFOCUS2 = '/focus', '/pffocus'
function SlashCmdList.PFFOCUS(msg)
if not pfUI.uf or not pfUI.uf.focus then return end
if msg ~= "" then
pfUI.uf.focus.unitname = strlower(msg)
elseif UnitName("target") then
pfUI.uf.focus.unitname = strlower(UnitName("target"))
-- Try to resolve GUID via short target swap
if UnitExists then
local _, prevGUID = UnitExists("target")
local prevPlayer = UnitIsUnit("target", "player")
-- Suppress "Unknown unit" errors during targeting attempts (fired async)
UIErrorsFrame:UnregisterEvent("UI_ERROR_MESSAGE")
-- Try exact match first, then prefix match via /tar
TargetByName(msg, true)
local _, guid = UnitExists("target")
if not guid or guid == "0x0000000000000000" then
-- Fallback: prefix match (like /tar storm -> Stormwind Guard)
SlashCmdList.TARGET(msg)
_, guid = UnitExists("target")
end
-- Re-enable errors next frame (errors are fired async)
local restore = CreateFrame("Frame")
restore:SetScript("OnUpdate", function()
UIErrorsFrame:RegisterEvent("UI_ERROR_MESSAGE")
restore:SetScript("OnUpdate", nil)
end)
-- Restore previous target
if prevGUID and prevGUID ~= "0x0000000000000000" then
TargetUnit(prevGUID)
elseif prevPlayer then
TargetUnit("player")
else
ClearTarget()
end
if guid and guid ~= "0x0000000000000000" then
SetFocusByGUID(guid)
return
end
end
-- Fallback: name-based (non-Nampower clients)
SetFocusByName(msg)
else
pfUI.uf.focus.unitname = nil
pfUI.uf.focus.label = nil
-- No msg: use current target
if UnitExists then
local _, guid = UnitExists("target")
if guid and guid ~= "0x0000000000000000" then
SetFocusByGUID(guid)
return
end
end
-- Fallback: name-based
local name = UnitName("target")
if name then
SetFocusByName(name)
end
end
end
@@ -53,11 +131,56 @@ function SlashCmdList.PFCASTFOCUS(msg)
return
end
local func = pfUI.api.TryMemoizedFuncLoadstringForSpellCasts(msg)
local focusGUID = pfUI.uf.focus.label
local hasGUID = focusGUID and focusGUID ~= "" and focusGUID ~= "0x0000000000000000"
-- GUID-based cast (Nampower) - no target toggle needed
if hasGUID and CastSpellByName and not func then
CastSpellByName(msg, focusGUID)
return
end
-- For lua functions with GUID: short target swap via GUID
if hasGUID and func then
local _, currentGUID = UnitExists("target")
local isPlayer = UnitIsUnit("target", "player")
TargetUnit(focusGUID)
local _, newGUID = UnitExists("target")
if newGUID ~= focusGUID then
-- Could not target focus, restore and fail
if currentGUID and currentGUID ~= "0x0000000000000000" then
TargetUnit(currentGUID)
elseif isPlayer then
TargetUnit("player")
else
TargetLastTarget()
end
UIErrorsFrame:AddMessage(SPELL_FAILED_BAD_TARGETS, 1, 0, 0)
return
end
func()
if currentGUID and currentGUID ~= "0x0000000000000000" then
TargetUnit(currentGUID)
elseif isPlayer then
TargetUnit("player")
else
TargetLastTarget()
end
return
end
-- Fallback: name-based target swap (no Nampower / no GUID)
local skiptarget = false
local player = UnitIsUnit("target", "player")
local unitname = ""
if pfUI.uf.focus.label and UnitIsUnit("target", pfUI.uf.focus.label .. pfUI.uf.focus.id) then
if pfUI.uf.focus.label and pfUI.uf.focus.id and
UnitIsUnit("target", pfUI.uf.focus.label .. pfUI.uf.focus.id) then
skiptarget = true
else
pfScanActive = true
@@ -69,7 +192,7 @@ function SlashCmdList.PFCASTFOCUS(msg)
TargetByName(pfUI.uf.focus.unitname, true)
end
if strlower(UnitName("target")) ~= strlower(unitname) then
if strlower(UnitName("target") or "") ~= strlower(unitname or "") then
pfScanActive = nil
TargetLastTarget()
UIErrorsFrame:AddMessage(SPELL_FAILED_BAD_TARGETS, 1, 0, 0)
@@ -77,7 +200,6 @@ function SlashCmdList.PFCASTFOCUS(msg)
end
end
local func = loadstring(msg or "")
if func then
func()
else
@@ -98,9 +220,26 @@ SLASH_PFSWAPFOCUS1, SLASH_PFSWAPFOCUS2 = '/swapfocus', '/pfswapfocus'
function SlashCmdList.PFSWAPFOCUS(msg)
if not pfUI.uf or not pfUI.uf.focus then return end
local oldunit = UnitExists("target") and strlower(UnitName("target"))
if oldunit and pfUI.uf.focus.unitname then
TargetByName(pfUI.uf.focus.unitname)
pfUI.uf.focus.unitname = oldunit
local _, guid = nil, nil
if UnitExists then
_, guid = UnitExists("target")
end
end
if guid and guid ~= "0x0000000000000000" then
local oldGUID = pfUI.uf.focus.label
SetFocusByGUID(guid)
-- Target old focus if we had one
if oldGUID and oldGUID ~= "" and oldGUID ~= "0x0000000000000000" then
TargetUnit(oldGUID)
end
else
-- Fallback: name-based swap
local oldunit = UnitExists("target") and strlower(UnitName("target") or "")
if oldunit and pfUI.uf.focus.unitname then
TargetByName(pfUI.uf.focus.unitname, true)
pfUI.uf.focus.unitname = oldunit
end
end
end
+389 -14
View File
@@ -351,13 +351,13 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
entry.text = text
entry.func = function()
if category[config] ~= value then
if category and category[config] ~= value then
category[config] = value
if ufunc then ufunc() else pfUI.gui.settingChanged = true end
end
end
if category[config] == value then
if category and category[config] == value then
frame.input.current = i
end
@@ -656,7 +656,11 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
pfUI.gui.hoverbind:SetHeight(25)
pfUI.gui.hoverbind:SetText(T["Hoverbind"])
pfUI.gui.hoverbind:SetScript("OnClick", function()
if pfUI.hoverbind then pfUI.hoverbind:Show() end
if pfUI.hoverbind then
pfUI.hoverbind:Show()
else
message("Please enable the Hoverbind module to use this feature.")
end
end)
SkinButton(pfUI.gui.hoverbind)
@@ -671,6 +675,8 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
if pfShare then
pfShare:Show()
pfShareExport:Click()
else
message("Please enable the Share module to share your config.")
end
end)
SkinButton(pfUI.gui.share)
@@ -885,6 +891,15 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
"7:" .. T["Small"],
"8:" .. T["Tiny (PixelPerfect)"],
},
["abbrevnum"] = {
"0:" .. T["Full Numbers (4250)"],
"1:" .. T["Abbreviate 2 Decimals (4.25k)"],
"2:" .. T["Abbreviate 1 Decimal (4.2k)"],
},
["castbardecimals"] = {
"1:" .. T["1 Decimal (2.1)"],
"2:" .. T["2 Decimals (2.14)"],
},
["orientation"] = {
"HORIZONTAL:" .. T["Horizontal"],
"VERTICAL:" .. T["Vertical"],
@@ -1372,7 +1387,7 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
donate:SetHeight(20)
donate:SetText(T["Donate"])
donate:SetScript("OnClick", function()
pfUI.chat.urlcopy.CopyText("https://ko-fi.com/shagu")
pfUI.chat.urlcopy.CopyText("https://buymeacoffee.com/w1ot8abps4")
end)
SkinButton(donate)
@@ -1382,7 +1397,7 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
github:SetHeight(20)
github:SetText(T["GitHub"])
github:SetScript("OnClick", function()
pfUI.chat.urlcopy.CopyText("https://github.com/shagu/pfUI")
pfUI.chat.urlcopy.CopyText("https://github.com/me0wg4ming/pfUI")
end)
SkinButton(github)
@@ -1392,7 +1407,7 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
website:SetHeight(20)
website:SetText(T["Website"])
website:SetScript("OnClick", function()
pfUI.chat.urlcopy.CopyText("https://shagu.org/pfUI")
pfUI.chat.urlcopy.CopyText("https://github.com/me0wg4ming/pfUI")
end)
SkinButton(website)
end)
@@ -1490,7 +1505,8 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
CreateConfig(nil, T["Disable Errors in UIErrors Frame"], C.global, "errors_hide", "checkbox")
CreateConfig(nil, T["Highlight Settings That Require Reload"], C.gui, "reloadmarker", "checkbox")
CreateConfig(nil, T["Show Incompatible Config Entries"], C.gui, "showdisabled", "checkbox")
CreateConfig(nil, T["Abbreviate Numbers (4200 -> 4.2k)"], C.unitframes, "abbrevnum", "checkbox")
CreateConfig(nil, T["Abbreviate Numbers"], C.unitframes, "abbrevnum", "dropdown", pfUI.gui.dropdowns.abbrevnum)
CreateConfig(nil, T["Castbar Timer Decimals"], C.unitframes, "castbardecimals", "dropdown", pfUI.gui.dropdowns.castbardecimals)
CreateConfig(nil, T["Abbreviate Unit Names"], C.unitframes, "abbrevname", "checkbox")
CreateConfig(nil, T["Health Point Estimation"], nil, nil, "header")
CreateConfig(nil, T["Estimate Enemy Health Points"], C.global, "libhealth", "checkbox")
@@ -1584,7 +1600,6 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
CreateConfig(nil, T["Cooldown Text Font Size (Blizzard Frames)"], C.appearance.cd, "font_size_blizz")
CreateConfig(nil, T["Cooldown Text Font Size (Foreign Frames)"], C.appearance.cd, "font_size_foreign")
CreateConfig(nil, T["Cooldown Text Time Threshold"], C.appearance.cd, "threshold")
CreateConfig(nil, T["Display Debuff Durations"], C.appearance.cd, "debuffs", "checkbox")
CreateConfig(nil, T["Enable Durations On Blizzard Frames"], C.appearance.cd, "blizzard", "checkbox")
CreateConfig(nil, T["Enable Durations On Foreign Frames"], C.appearance.cd, "foreign", "checkbox")
CreateConfig(nil, T["Hide Foreign Cooldown Animations"], C.appearance.cd, "hideanim", "checkbox")
@@ -1610,6 +1625,321 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
CreateConfig(nil, T["Selected Core"], C.gm, "server", "dropdown", pfUI.gui.dropdowns.gmserver_text)
end)
-- Throttling Menu
CreateGUIEntry(T["Throttling"], T["Nameplates"], function()
local header = CreateConfig(nil, T["Nameplate Update Rate"], nil, nil, "header")
header:GetParent().objectCount = header:GetParent().objectCount - 1
header:SetHeight(20)
local targetCustom -- declare first so callback can use it
CreateConfig(function()
-- Callback when dropdown changes - update custom field immediately
if targetCustom and targetCustom.input then
local isCustom = pfUI.throttle:IsCustom("nameplates_target")
if not isCustom then
-- Preset selected - show FPS from preset, make readonly
targetCustom.input:EnableMouse(false)
targetCustom.input:EnableKeyboard(false)
targetCustom.input:ClearFocus()
targetCustom.input:SetTextColor(.5,.5,.5,1)
targetCustom.input:SetText(tostring(pfUI.throttle:GetFps("nameplates_target")))
else
-- Custom selected - make editable
targetCustom.input:EnableMouse(true)
targetCustom.input:EnableKeyboard(true)
targetCustom.input:SetTextColor(.2,1,.8,1)
if _G.pfUI_throttle.nameplates_target_custom then
targetCustom.input:SetText(_G.pfUI_throttle.nameplates_target_custom)
end
end
end
end, T["Target/Casting Plates"], _G.pfUI_throttle, "nameplates_target", "dropdown", {
"very_slow:" .. T["Very Slow"] .. " (2 FPS)",
"slow:" .. T["Slow"] .. " (5 FPS)",
"normal:" .. T["Normal"] .. " (10 FPS)",
"fast:" .. T["Fast"] .. " (20 FPS)",
"very_fast:" .. T["Very Fast"] .. " (30 FPS)",
"fastest:" .. T["Fastest"] .. " (50 FPS)",
"custom:" .. T["Custom"],
})
-- Now create custom field AFTER dropdown
targetCustom = CreateConfig(nil, T["Custom FPS"], _G.pfUI_throttle, "nameplates_target_custom")
-- Set initial state
local isCustom = pfUI.throttle:IsCustom("nameplates_target")
if not isCustom then
targetCustom.input:EnableMouse(false)
targetCustom.input:EnableKeyboard(false)
targetCustom.input:SetTextColor(.5,.5,.5,1)
targetCustom.input:SetText(tostring(pfUI.throttle:GetFps("nameplates_target")))
else
targetCustom.input:EnableMouse(true)
targetCustom.input:EnableKeyboard(true)
targetCustom.input:SetTextColor(.2,1,.8,1)
if _G.pfUI_throttle.nameplates_target_custom then
targetCustom.input:SetText(_G.pfUI_throttle.nameplates_target_custom)
end
end
-- Spacer after custom field
local spacer1 = CreateConfig(nil, " ", nil, nil, "header")
spacer1:GetParent().objectCount = spacer1:GetParent().objectCount - 1
spacer1:SetHeight(5)
local normalCustom
CreateConfig(function()
if normalCustom and normalCustom.input then
local isCustom = pfUI.throttle:IsCustom("nameplates")
if not isCustom then
normalCustom.input:EnableMouse(false)
normalCustom.input:EnableKeyboard(false)
normalCustom.input:ClearFocus()
normalCustom.input:SetTextColor(.5,.5,.5,1)
normalCustom.input:SetText(tostring(pfUI.throttle:GetFps("nameplates")))
else
normalCustom.input:EnableMouse(true)
normalCustom.input:EnableKeyboard(true)
normalCustom.input:SetTextColor(.2,1,.8,1)
if _G.pfUI_throttle.nameplates_custom then
normalCustom.input:SetText(_G.pfUI_throttle.nameplates_custom)
end
end
end
end, T["Normal Plates"], _G.pfUI_throttle, "nameplates", "dropdown", {
"very_slow:" .. T["Very Slow"] .. " (2 FPS)",
"slow:" .. T["Slow"] .. " (5 FPS)",
"normal:" .. T["Normal"] .. " (10 FPS)",
"fast:" .. T["Fast"] .. " (20 FPS)",
"very_fast:" .. T["Very Fast"] .. " (30 FPS)",
"fastest:" .. T["Fastest"] .. " (50 FPS)",
"custom:" .. T["Custom"],
})
normalCustom = CreateConfig(nil, T["Custom FPS"], _G.pfUI_throttle, "nameplates_custom")
local isCustom2 = pfUI.throttle:IsCustom("nameplates")
if not isCustom2 then
normalCustom.input:EnableMouse(false)
normalCustom.input:EnableKeyboard(false)
normalCustom.input:SetTextColor(.5,.5,.5,1)
normalCustom.input:SetText(tostring(pfUI.throttle:GetFps("nameplates")))
else
normalCustom.input:EnableMouse(true)
normalCustom.input:EnableKeyboard(true)
normalCustom.input:SetTextColor(.2,1,.8,1)
if _G.pfUI_throttle.nameplates_custom then
normalCustom.input:SetText(_G.pfUI_throttle.nameplates_custom)
end
end
-- Spacer after custom field
local spacer2 = CreateConfig(nil, " ", nil, nil, "header")
spacer2:GetParent().objectCount = spacer2:GetParent().objectCount - 1
spacer2:SetHeight(5)
local massCustom
CreateConfig(function()
if massCustom and massCustom.input then
local isCustom = pfUI.throttle:IsCustom("nameplates_mass")
if not isCustom then
massCustom.input:EnableMouse(false)
massCustom.input:EnableKeyboard(false)
massCustom.input:ClearFocus()
massCustom.input:SetTextColor(.5,.5,.5,1)
massCustom.input:SetText(tostring(pfUI.throttle:GetFps("nameplates_mass")))
else
massCustom.input:EnableMouse(true)
massCustom.input:EnableKeyboard(true)
massCustom.input:SetTextColor(.2,1,.8,1)
if _G.pfUI_throttle.nameplates_mass_custom then
massCustom.input:SetText(_G.pfUI_throttle.nameplates_mass_custom)
end
end
end
end, T["Mass Pulls (20+ Plates)"], _G.pfUI_throttle, "nameplates_mass", "dropdown", {
"very_slow:" .. T["Very Slow"] .. " (2 FPS)",
"slow:" .. T["Slow"] .. " (5 FPS)",
"normal:" .. T["Normal"] .. " (10 FPS)",
"fast:" .. T["Fast"] .. " (20 FPS)",
"very_fast:" .. T["Very Fast"] .. " (30 FPS)",
"fastest:" .. T["Fastest"] .. " (50 FPS)",
"custom:" .. T["Custom"],
})
massCustom = CreateConfig(nil, T["Custom FPS"], _G.pfUI_throttle, "nameplates_mass_custom")
local isCustom3 = pfUI.throttle:IsCustom("nameplates_mass")
if not isCustom3 then
massCustom.input:EnableMouse(false)
massCustom.input:EnableKeyboard(false)
massCustom.input:SetTextColor(.5,.5,.5,1)
massCustom.input:SetText(tostring(pfUI.throttle:GetFps("nameplates_mass")))
else
massCustom.input:EnableMouse(true)
massCustom.input:EnableKeyboard(true)
massCustom.input:SetTextColor(.2,1,.8,1)
if _G.pfUI_throttle.nameplates_mass_custom then
massCustom.input:SetText(_G.pfUI_throttle.nameplates_mass_custom)
end
end
-- Spacer before reset button
local spacer = CreateConfig(nil, " ", nil, nil, "header")
spacer:GetParent().objectCount = spacer:GetParent().objectCount - 1
spacer:SetHeight(10)
-- Reset to defaults button
CreateConfig(nil, T["Reset to Defaults"], nil, nil, "button", function()
pfUI.throttle:ResetToDefault("nameplates_target")
pfUI.throttle:ResetToDefault("nameplates")
pfUI.throttle:ResetToDefault("nameplates_mass")
-- Also reset custom fields to their default FPS values
_G.pfUI_throttle.nameplates_target_custom = "50"
_G.pfUI_throttle.nameplates_custom = "10"
_G.pfUI_throttle.nameplates_mass_custom = "7"
Reload()
end, true)
end)
CreateGUIEntry(T["Throttling"], T["Tooltips"], function()
local header = CreateConfig(nil, T["Tooltip Update Rate"], nil, nil, "header")
header:GetParent().objectCount = header:GetParent().objectCount - 1
header:SetHeight(20)
local cursorCustom
CreateConfig(function()
if cursorCustom and cursorCustom.input then
local isCustom = pfUI.throttle:IsCustom("tooltip_cursor")
if not isCustom then
cursorCustom.input:EnableMouse(false)
cursorCustom.input:EnableKeyboard(false)
cursorCustom.input:ClearFocus()
cursorCustom.input:SetTextColor(.5,.5,.5,1)
cursorCustom.input:SetText(tostring(pfUI.throttle:GetFps("tooltip_cursor")))
else
cursorCustom.input:EnableMouse(true)
cursorCustom.input:EnableKeyboard(true)
cursorCustom.input:SetTextColor(.2,1,.8,1)
if _G.pfUI_throttle.tooltip_cursor_custom then
cursorCustom.input:SetText(_G.pfUI_throttle.tooltip_cursor_custom)
end
end
end
end, T["Cursor Follow"], _G.pfUI_throttle, "tooltip_cursor", "dropdown", {
"very_slow:" .. T["Very Slow"] .. " (2 FPS)",
"slow:" .. T["Slow"] .. " (5 FPS)",
"normal:" .. T["Normal"] .. " (10 FPS)",
"fast:" .. T["Fast"] .. " (20 FPS)",
"very_fast:" .. T["Very Fast"] .. " (30 FPS)",
"fastest:" .. T["Fastest"] .. " (50 FPS)",
"custom:" .. T["Custom"],
})
cursorCustom = CreateConfig(nil, T["Custom FPS"], _G.pfUI_throttle, "tooltip_cursor_custom")
local isCustom = pfUI.throttle:IsCustom("tooltip_cursor")
if not isCustom then
cursorCustom.input:EnableMouse(false)
cursorCustom.input:EnableKeyboard(false)
cursorCustom.input:SetTextColor(.5,.5,.5,1)
cursorCustom.input:SetText(tostring(pfUI.throttle:GetFps("tooltip_cursor")))
else
cursorCustom.input:EnableMouse(true)
cursorCustom.input:EnableKeyboard(true)
cursorCustom.input:SetTextColor(.2,1,.8,1)
if _G.pfUI_throttle.tooltip_cursor_custom then
cursorCustom.input:SetText(_G.pfUI_throttle.tooltip_cursor_custom)
end
end
-- Small spacer
local spacer1 = CreateConfig(nil, " ", nil, nil, "header")
spacer1:GetParent().objectCount = spacer1:GetParent().objectCount - 1
spacer1:SetHeight(5)
-- Info note about Native mode
local infoText = CreateConfig(nil, T["Note: Only works when Cursor Align is NOT 'Native'"], nil, nil, "header")
infoText:GetParent().objectCount = infoText:GetParent().objectCount - 1
infoText:SetHeight(25)
-- Spacer before reset button
local spacer = CreateConfig(nil, " ", nil, nil, "header")
spacer:GetParent().objectCount = spacer:GetParent().objectCount - 1
spacer:SetHeight(5)
-- Reset to defaults button
CreateConfig(nil, T["Reset to Defaults"], nil, nil, "button", function()
pfUI.throttle:ResetToDefault("tooltip_cursor")
_G.pfUI_throttle.tooltip_cursor_custom = "10"
Reload()
end, true)
end)
CreateGUIEntry(T["Throttling"], T["Chat Tab"], function()
local chatCustom
CreateConfig(function()
if chatCustom and chatCustom.input then
local isCustom = pfUI.throttle:IsCustom("chat_tab")
if not isCustom then
chatCustom.input:EnableMouse(false)
chatCustom.input:EnableKeyboard(false)
chatCustom.input:ClearFocus()
chatCustom.input:SetTextColor(.5,.5,.5,1)
chatCustom.input:SetText(tostring(pfUI.throttle:GetFps("chat_tab")))
else
chatCustom.input:EnableMouse(true)
chatCustom.input:EnableKeyboard(true)
chatCustom.input:SetTextColor(.2,1,.8,1)
if _G.pfUI_throttle.chat_tab_custom then
chatCustom.input:SetText(_G.pfUI_throttle.chat_tab_custom)
end
end
end
end, T["Chat Tab Hover Check"], _G.pfUI_throttle, "chat_tab", "dropdown", {
"very_slow:" .. T["Very Slow"] .. " (2 FPS)",
"slow:" .. T["Slow"] .. " (5 FPS)",
"normal:" .. T["Normal"] .. " (10 FPS)",
"fast:" .. T["Fast"] .. " (20 FPS)",
"very_fast:" .. T["Very Fast"] .. " (30 FPS)",
"fastest:" .. T["Fastest"] .. " (50 FPS)",
"custom:" .. T["Custom"],
})
chatCustom = CreateConfig(nil, T["Custom FPS"], _G.pfUI_throttle, "chat_tab_custom")
local isCustom = pfUI.throttle:IsCustom("chat_tab")
if not isCustom then
chatCustom.input:EnableMouse(false)
chatCustom.input:EnableKeyboard(false)
chatCustom.input:SetTextColor(.5,.5,.5,1)
chatCustom.input:SetText(tostring(pfUI.throttle:GetFps("chat_tab")))
else
chatCustom.input:EnableMouse(true)
chatCustom.input:EnableKeyboard(true)
chatCustom.input:SetTextColor(.2,1,.8,1)
if _G.pfUI_throttle.chat_tab_custom then
chatCustom.input:SetText(_G.pfUI_throttle.chat_tab_custom)
end
end
-- Spacer before reset button
local spacer = CreateConfig(nil, " ", nil, nil, "header")
spacer:GetParent().objectCount = spacer:GetParent().objectCount - 1
spacer:SetHeight(10)
-- Reset to defaults button
CreateConfig(nil, T["Reset to Defaults"], nil, nil, "button", function()
pfUI.throttle:ResetToDefault("chat_tab")
_G.pfUI_throttle.chat_tab_custom = "10"
Reload()
end, true)
end)
CreateGUIEntry(T["Unit Frames"], T["General"], function()
CreateConfig(nil, T["Disable pfUI Unit Frames"], C.unitframes, "disable", "checkbox")
CreateConfig(nil, T["Healthbar Animation Speed"], C.unitframes, "animation_speed", "dropdown", pfUI.gui.dropdowns.uf_animationspeed)
@@ -1624,6 +1954,21 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
CreateConfig(nil, T["Enable Mana Ticks"], C.unitframes.player, "manatick", "checkbox")
CreateConfig(nil, T["Detect Enemy Buffs"], C.unitframes, "buffdetect", "checkbox", nil, nil, nil, nil, "vanilla" )
CreateConfig(nil, T["Swing Timer"], nil, nil, "header")
CreateConfig(nil, T["Swing Timer Width"], C.unitframes, "swingtimerwidth")
CreateConfig(nil, T["Swing Timer Height"], C.unitframes, "swingtimerheight")
CreateConfig(nil, T["Swing Timer Texture"], C.unitframes, "swingtimertexture", "dropdown", pfUI.gui.dropdowns.uf_bartexture)
CreateConfig(nil, T["Swing Timer Font Size"], C.unitframes, "swingtimerfontsize")
CreateConfig(nil, T["Show Timer Text"], C.unitframes, "swingtimertext", "checkbox")
CreateConfig(nil, T["Show MH/OH Labels"], C.unitframes, "swingtimerlabel", "checkbox")
CreateConfig(nil, T["Show Offhand Bar"], C.unitframes, "swingtimeroffhand", "checkbox")
CreateConfig(nil, T["Show Ranged Bar"], C.unitframes, "swingtimerranged", "checkbox")
CreateConfig(nil, T["Mainhand Bar Color"], C.unitframes, "swingtimermhcolor", "color")
CreateConfig(nil, T["Offhand Bar Color"], C.unitframes, "swingtimerohcolor", "color")
CreateConfig(nil, T["Ranged Bar Color"], C.unitframes, "swingtimerrangedcolor", "color")
CreateConfig(nil, T["Ranged Warn Color (Hunter)"], C.unitframes, "swingtimerrangedwarncolor", "color")
CreateConfig(nil, T["Show HS/Cleave Queue Color (Warrior)"], C.unitframes, "swingtimerhsqueue", "checkbox")
CreateConfig(U[c], T["Font Options"], nil, nil, "header")
CreateConfig(nil, T["Unit Frame Text Font"], C.global, "font_unit", "dropdown", pfUI.gui.dropdowns.fonts)
CreateConfig(nil, T["Unit Frame Text Size"], C.global, "font_unit_size")
@@ -1653,10 +1998,29 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
CreateConfig(nil, T["Energy Color"], C.unitframes, "energycolor", "color")
CreateConfig(nil, T["Focus Color"], C.unitframes, "focuscolor", "color")
CreateConfig(nil, T["SuperWoW Settings"], nil, nil, "header")
CreateConfig(nil, T["Druid Settings"], nil, nil, "header")
CreateConfig(nil, T["Show Druid Mana Bar"], C.unitframes, "druidmanabar", "checkbox", nil, nil, nil, nil, "vanilla" )
CreateConfig(nil, T["Druid Mana Bar Height"], C.unitframes, "druidmanaheight", nil, nil, nil, nil, nil, "vanilla" )
CreateConfig(nil, T["Druid Mana Bar Text"], C.unitframes, "druidmanatext", "checkbox", nil, nil, nil, nil, "vanilla" )
CreateConfig(nil, T["Druid Mana Bar Width (-1 = auto)"], C.unitframes, "druidmanawidth", nil, nil, nil, nil, nil, "vanilla" )
CreateConfig(nil, T["Druid Mana Bar X-Offset"], C.unitframes, "druidmanaoffx", nil, nil, nil, nil, nil, "vanilla" )
CreateConfig(nil, T["Druid Mana Bar Y-Offset"], C.unitframes, "druidmanaoffy", nil, nil, nil, nil, nil, "vanilla" )
CreateConfig(nil, T["Druid Mana Bar Spacing"], C.unitframes, "druidmanaspace", nil, nil, nil, nil, nil, "vanilla" )
CreateConfig(nil, T["Druid Mana Bar Texture"], C.unitframes, "druidmanatexture", "dropdown", pfUI.gui.dropdowns.uf_bartexture, nil, nil, nil, "vanilla" )
CreateConfig(nil, T["SuperWoW Settings"], nil, nil, "header")
CreateConfig(nil, T["Track Group on Minimap"], C.unitframes, "track_group", "checkbox", nil, nil, nil, nil, "vanilla" )
CreateConfig(nil, T["Nampower Settings"], nil, nil, "header")
CreateConfig(nil, T["Show Spell Queue Indicator"], C.unitframes, "spellqueue", "checkbox", nil, nil, nil, nil, "vanilla" )
CreateConfig(nil, T["Spell Queue Icon Size"], C.unitframes, "spellqueuesize", nil, nil, nil, nil, nil, "vanilla" )
CreateConfig(nil, T["Show Reactive Spell Indicator"], C.unitframes, "reactive_indicator", "checkbox", nil, nil, nil, nil, "vanilla" )
CreateConfig(nil, T["Reactive Indicator Size"], C.unitframes, "reactive_size", nil, nil, nil, nil, nil, "vanilla" )
CreateConfig(nil, T["UnitXP Settings"], nil, nil, "header")
CreateConfig(nil, T["Show Line of Sight Indicator"], C.unitframes, "los_indicator", "checkbox", nil, nil, nil, nil, "vanilla" )
CreateConfig(nil, T["Show Behind Indicator"], C.unitframes, "behind_indicator", "checkbox", nil, nil, nil, nil, "vanilla" )
CreateConfig(nil, T["Enable OS Notifications"], C.unitframes, "unitxp_notify", "checkbox", nil, nil, nil, nil, "vanilla" )
end)
-- Shared Unit- and Groupframes
@@ -1813,7 +2177,7 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
CreateConfig(U[c], T["Timer"], nil, nil, "header")
CreateConfig(U[c], T["Show Timer Text"], C.unitframes[c], "cooldown_text", "checkbox")
CreateConfig(U[c], T["Show Timer Animation"], C.unitframes[c], "cooldown_anim", "checkbox")
CreateConfig(Reload, T["Show Timer Animation"], C.unitframes[c], "cooldown_anim", "checkbox")
CreateConfig(U[c], T["Buffs"], nil, nil, "header")
CreateConfig(U[c], T["Buff Position"], C.unitframes[c], "buffs", "dropdown", pfUI.gui.dropdowns.uf_buff_position)
@@ -2014,7 +2378,9 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
CreateConfig(U["bars"], T["Button Animation"], C.bars, "animation", "dropdown", pfUI.gui.dropdowns.actionbuttonanimations)
CreateConfig(U["bars"], T["Button Animation Trigger"], C.bars, "animmode", "dropdown", pfUI.gui.dropdowns.animationmode)
CreateConfig(U["bars"], T["Show Animation On Hidden Bars"], C.bars, "animalways", "checkbox")
CreateConfig(U["bars"], T["Scan Macros For Spells"], C.bars, "macroscan", "checkbox", nil, nil, nil, nil, "vanilla")
if not pfUI:MacroAddonsLoaded() then
CreateConfig(U["bars"], T["Scan Macros For Spells"], C.bars, "macroscan", "checkbox", nil, nil, nil, nil, "vanilla")
end
CreateConfig(U["bars"], T["Show Reagent Count"], C.bars, "reagents", "checkbox")
CreateConfig(U["bars"], T["Highlight Equipped Items"], C.bars, "showequipped", "checkbox")
CreateConfig(U["bars"], T["Equipped Item Color"], C.bars, "eqcolor", "color")
@@ -2263,6 +2629,7 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
CreateConfig(nil, T["Generate Playerlinks"], C.chat.text, "playerlinks", "checkbox")
CreateConfig(nil, T["Enable URL Detection"], C.chat.text, "detecturl", "checkbox")
CreateConfig(nil, T["Enable Class Colors"], C.chat.text, "classcolor", "checkbox")
CreateConfig(nil, T["Enable Player Levels"], C.chat.text, "playerlevel", "checkbox")
CreateConfig(nil, T["Who Search Unknown Classes (|cffffaaaaExperimental|r)"], C.chat.text, "whosearchunknown", "checkbox")
CreateConfig(nil, T["Colorize Unknown Classes"], C.chat.text, "tintunknown", "checkbox")
CreateConfig(nil, T["Unknown Class Color"], C.chat.text, "unknowncolor", "color")
@@ -2294,6 +2661,8 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
CreateGUIEntry(T["Nameplates"], nil, function()
CreateConfig(U["nameplates"], T["Show On Hostile Units"], C.nameplates, "showhostile", "checkbox")
CreateConfig(U["nameplates"], T["Show On Friendly Units"], C.nameplates, "showfriendly", "checkbox")
CreateConfig(U["nameplates"], T["Disable Hostile Nameplates In Friendly Zones"], C.nameplates, "disable_hostile_in_friendly", "checkbox")
CreateConfig(U["nameplates"], T["Disable Friendly Nameplates In Friendly Zones"], C.nameplates, "disable_friendly_in_friendly", "checkbox")
CreateConfig(U["nameplates"], T["Vertical Offset (|cffffaaaaExperimental|r)"], C.nameplates, "vertical_offset", nil, nil, nil, nil, nil, "vanilla")
CreateConfig(U["nameplates"], T["Inactive Nameplate Alpha"], C.nameplates, "notargalpha", "dropdown", pfUI.gui.dropdowns.percent_small)
CreateConfig(U["nameplates"], T["Draw Glow Around Target Nameplate"], C.nameplates, "targetglow", "checkbox")
@@ -2334,6 +2703,10 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
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")
CreateConfig(Reload, T["Show Timer Animation"], C.nameplates, "debuffanim", "checkbox")
CreateConfig(U["nameplates"], T["Only Show Own Debuffs (|cffffaaaaExperimental|r)"], C.nameplates, "selfdebuff", "checkbox")
CreateConfig(U["nameplates"], T["Filter Mode"], C.nameplates.debuffs, "filter", "dropdown", pfUI.gui.dropdowns.buffbarfilter)
CreateConfig(U["nameplates"], T["Blacklist"], C.nameplates.debuffs, "blacklist", "list")
@@ -2406,6 +2779,7 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
CreateConfig(nil, "BetterCharacterStats", C.thirdparty.bcs, "enable", "checkbox", nil, nil, nil, nil, "vanilla")
CreateConfig(nil, "Crafty", C.thirdparty.crafty, "enable", "checkbox", nil, nil, nil, nil, "vanilla")
CreateConfig(nil, "CleverMacro", C.thirdparty.clevermacro, "enable", "checkbox", nil, nil, nil, nil, "vanilla")
CreateConfig(nil, "SuperCleveRoidMacros", C.thirdparty.supercleveroidmacros, "enable", "checkbox", nil, nil, nil, nil, "vanilla")
CreateConfig(nil, "AckisRecipeList", C.thirdparty.ackis, "enable", "checkbox", nil, nil, nil, nil, "tbc")
CreateConfig(nil, "SheepWatch", C.thirdparty.sheepwatch, "enable", "checkbox", nil, nil, nil, nil, "tbc")
CreateConfig(nil, "TotemTimers", C.thirdparty.totemtimers, "enable", "checkbox", nil, nil, nil, nil, "tbc")
@@ -2423,7 +2797,8 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
CreateGUIEntry(T["Components"], T["Modules"], function()
table.sort(pfUI.modules)
for i,m in pairs(pfUI.modules) do
if m ~= "gui" then
-- skip gui and macrotweak when macro addons are loaded
if m ~= "gui" and not (m == "macrotweak" and pfUI:MacroAddonsLoaded()) then
-- create disabled entry if not existing and display
pfUI:UpdateConfig("disabled", nil, m, "0")
CreateConfig(nil, T["Disable Module"] .. " " .. m, C.disabled, m, "checkbox")
@@ -2440,4 +2815,4 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
end
end)
end
end)
end)
+3
View File
@@ -1,4 +1,7 @@
pfUI:RegisterModule("macrotweak", "vanilla", function ()
-- disable macrotweak when macro addons are loaded
if IsAddOnLoaded("Supermacro") or IsAddOnLoaded("SuperCleveRoidMacros") or IsAddOnLoaded("UltimaMacros") then return end
-- do not write macro calls into chat input history
if ChatFrameEditBox._AddHistoryLine then
local userinput
+5 -2
View File
@@ -134,8 +134,11 @@ pfUI:RegisterModule("minimap", "vanilla:tbc", function ()
-- Create coordinates text frame with location configurable
pfUI.minimapCoordinates = CreateFrame("Frame", "pfMinimapCoord", pfUI.minimap)
pfUI.minimapCoordinates:SetScript("OnUpdate", function()
-- update coords every 0.1 seconds
if C.appearance.minimap.coordstext ~= "off" and ( this.tick or .1) > GetTime() then return else this.tick = GetTime() + .1 end
-- Throttle to update coords every 0.1 seconds
if ( this.tick or 0) > GetTime() then return end
this.tick = GetTime() + .1
if C.appearance.minimap.coordstext == "off" then return end
this.posX, this.posY = GetPlayerMapPosition("player")
if this.posX ~= 0 and this.posY ~= 0 then
+9 -2
View File
@@ -34,7 +34,7 @@ pfUI:RegisterModule("mouseover", "vanilla", function ()
_G.SLASH_PFCAST1, _G.SLASH_PFCAST2 = "/pfcast", "/pfmouse"
function SlashCmdList.PFCAST(msg)
local restore_target = true
local func = loadstring(msg or "")
local func = pfUI.api.TryMemoizedFuncLoadstringForSpellCasts(msg)
local unit = "mouseover"
if not UnitExists(unit) then
@@ -50,6 +50,13 @@ pfUI:RegisterModule("mouseover", "vanilla", function ()
end
end
-- Nampower: CastSpellByName supports a second unit parameter directly.
-- unit is already resolved to "mouseover", "target" or "player" at this point.
if not func and GetNampowerVersion then
CastSpellByName(msg, unit)
return
end
-- If target and mouseover are friendly units, we can't use spell target as it
-- would cast on the target instead of the mouseover. However, if the mouseover
-- is friendly and the target is not, we can try to obtain the best unitstring
@@ -90,4 +97,4 @@ pfUI:RegisterModule("mouseover", "vanilla", function ()
TargetLastTarget()
end
end
end)
end)
+763 -213
View File
File diff suppressed because it is too large Load Diff
+672
View File
@@ -0,0 +1,672 @@
-- Nampower integration module
-- Provides spell queue indicator and enhanced cast information
-- Requires Nampower DLL: https://gitea.com/avitasia/nampower
pfUI:RegisterModule("nampower", "vanilla", function ()
-- Only load if Nampower is available
if not GetNampowerVersion then return end
-- Safe wrapper for SuperWoW's GetSpellNameAndRankForId (may not be available)
local function SafeGetSpellNameAndRank(spellId)
if not GetSpellNameAndRankForId then return nil, nil end
local success, name, rank = pcall(GetSpellNameAndRankForId, spellId)
if success then
return name, rank
end
return nil, nil
end
local rawborder, border = GetBorderSize()
-- Spell Queue Indicator
-- Shows the currently queued spell icon near the castbar
if C.unitframes.spellqueue == "1" then
local size = tonumber(C.unitframes.spellqueuesize) or 32
pfUI.spellqueue = CreateFrame("Frame", "pfSpellQueue", UIParent)
pfUI.spellqueue:SetFrameStrata("HIGH")
pfUI.spellqueue:SetWidth(size)
pfUI.spellqueue:SetHeight(size)
pfUI.spellqueue:Hide()
-- Position near player castbar if available
if pfUI.castbar and pfUI.castbar.player then
pfUI.spellqueue:SetPoint("LEFT", pfUI.castbar.player, "RIGHT", border*3, 0)
else
pfUI.spellqueue:SetPoint("CENTER", UIParent, "CENTER", 100, -100)
end
pfUI.spellqueue.icon = pfUI.spellqueue:CreateTexture("OVERLAY")
pfUI.spellqueue.icon:SetAllPoints(pfUI.spellqueue)
pfUI.spellqueue.icon:SetTexCoord(.08, .92, .08, .92)
UpdateMovable(pfUI.spellqueue)
CreateBackdrop(pfUI.spellqueue)
CreateBackdropShadow(pfUI.spellqueue)
-- Event codes from Nampower
local ON_SWING_QUEUED = 0
local ON_SWING_QUEUE_POPPED = 1
local NORMAL_QUEUED = 2
local NORMAL_QUEUE_POPPED = 3
local NON_GCD_QUEUED = 4
local NON_GCD_QUEUE_POPPED = 5
local queue = CreateFrame("Frame")
queue:RegisterEvent("SPELL_QUEUE_EVENT")
queue:RegisterEvent("PLAYER_LOGOUT")
queue:SetScript("OnEvent", function()
-- Handle shutdown to prevent crash 132
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
return
end
local eventCode = arg1
local spellId = arg2
if eventCode == NORMAL_QUEUED or eventCode == NON_GCD_QUEUED or eventCode == ON_SWING_QUEUED then
-- Get spell texture from GetSpellRec (Nampower) or SpellInfo (SuperWoW fallback)
local texture
if GetSpellRec then
local rec = GetSpellRec(spellId)
texture = rec and rec.spellIconID and GetSpellIconTexture(rec.spellIconID) or nil
elseif SpellInfo then
local _, _, tex = SpellInfo(spellId)
texture = tex
end
if texture then
pfUI.spellqueue.icon:SetTexture(texture)
pfUI.spellqueue:Show()
end
elseif eventCode == NORMAL_QUEUE_POPPED or eventCode == NON_GCD_QUEUE_POPPED or eventCode == ON_SWING_QUEUE_POPPED then
pfUI.spellqueue:Hide()
end
end)
end
-- NOTE: Buff tracking removed - was dead code (data collected but never used for display)
-- Direct Aura Access API using GetUnitField
-- Much faster than tooltip scanning - reads aura arrays directly from unit fields
if GetUnitField then
pfUI.api.GetUnitAuras = function(unit)
local auras = GetUnitField(unit, "aura")
local auraLevels = GetUnitField(unit, "auraLevels")
local auraStacks = GetUnitField(unit, "auraApplications")
if not auras then return nil end
local result = {}
for i = 1, 48 do
local spellId = auras[i]
if spellId and spellId > 0 then
local name, rank, texture
if GetSpellRec then
local rec = GetSpellRec(spellId)
if rec then
name = rec.name
rank = rec.rank
local iconID = rec.spellIconID
texture = iconID and GetSpellIconTexture(iconID) or nil
end
elseif SpellInfo then
name, rank, texture = SpellInfo(spellId)
end
if not name then
name, rank = SafeGetSpellNameAndRank(spellId)
end
result[i] = {
spellId = spellId,
name = name,
rank = rank,
texture = texture,
level = auraLevels and auraLevels[i] or 0,
stacks = auraStacks and auraStacks[i] or 1,
isBuff = i <= 32, -- First 32 slots are buffs, rest are debuffs
}
end
end
return result
end
-- Quick check if unit has specific aura by spellId
pfUI.api.UnitHasAura = function(unit, spellId)
local auras = GetUnitField(unit, "aura")
if not auras then return false end
for i = 1, 48 do
if auras[i] == spellId then return true, i end
end
return false
end
-- Get unit resistances directly
pfUI.api.GetUnitResistances = function(unit)
local res = GetUnitField(unit, "resistances")
if not res then return nil end
return {
armor = res[1] or 0,
holy = res[2] or 0,
fire = res[3] or 0,
nature = res[4] or 0,
frost = res[5] or 0,
shadow = res[6] or 0,
arcane = res[7] or 0
}
end
end
-- Reactive Spell Indicator using IsSpellUsable
-- Shows when reactive abilities like Overpower, Revenge, Execute are usable
if IsSpellUsable and C.unitframes.reactive_indicator == "1" then
local size = tonumber(C.unitframes.reactive_size) or 28
local _, class = UnitClass("player")
-- Reactive spells by class
local reactiveSpells = {
WARRIOR = {
{ name = "Overpower", texture = "Interface\\Icons\\Ability_MeleeDamage" },
{ name = "Revenge", texture = "Interface\\Icons\\Ability_Warrior_Revenge" },
{ name = "Execute", texture = "Interface\\Icons\\INV_Sword_48" },
},
ROGUE = {
{ name = "Riposte", texture = "Interface\\Icons\\Ability_Warrior_Challange" },
},
HUNTER = {
{ name = "Mongoose Bite", texture = "Interface\\Icons\\Ability_Hunter_SwiftStrike" },
{ name = "Counterattack", texture = "Interface\\Icons\\Ability_Warrior_Challange" },
},
}
local spells = reactiveSpells[class]
if spells then
pfUI.reactive = CreateFrame("Frame", "pfReactiveIndicator", UIParent)
pfUI.reactive:SetFrameStrata("HIGH")
local spellCount = table.getn(spells)
pfUI.reactive:SetWidth(size * spellCount + 4 * (spellCount - 1))
pfUI.reactive:SetHeight(size)
pfUI.reactive:SetPoint("CENTER", UIParent, "CENTER", 0, -200)
pfUI.reactive:Hide()
pfUI.reactive.icons = {}
for i, spell in ipairs(spells) do
local icon = CreateFrame("Frame", nil, pfUI.reactive)
icon:SetWidth(size)
icon:SetHeight(size)
icon:SetPoint("LEFT", pfUI.reactive, "LEFT", (i-1) * (size + 4), 0)
icon.texture = icon:CreateTexture(nil, "ARTWORK")
icon.texture:SetAllPoints(icon)
icon.texture:SetTexture(spell.texture)
icon.texture:SetTexCoord(.08, .92, .08, .92)
icon.glow = icon:CreateTexture(nil, "OVERLAY")
icon.glow:SetPoint("TOPLEFT", icon, "TOPLEFT", -4, 4)
icon.glow:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", 4, -4)
icon.glow:SetTexture(pfUI.media["img:glow"])
icon.glow:SetVertexColor(1, 1, 0, 0.8)
CreateBackdrop(icon)
icon:Hide()
icon.spellName = spell.name
pfUI.reactive.icons[i] = icon
end
UpdateMovable(pfUI.reactive)
pfUI.reactive:SetScript("OnUpdate", function()
local anyVisible = false
for _, icon in ipairs(this.icons) do
local usable = IsSpellUsable(icon.spellName)
if usable == 1 then
icon:Show()
anyVisible = true
else
icon:Hide()
end
end
if anyVisible then
this:Show()
else
this:Hide()
end
end)
end
end
-- Enhanced Cooldown Tracking API using GetSpellIdCooldown
if GetSpellIdCooldown then
pfUI.api.GetPreciseCooldown = function(spellId)
local cd = GetSpellIdCooldown(spellId)
if not cd then return nil end
return {
onCooldown = (cd.isOnCooldown or 0) == 1,
remaining = (cd.cooldownRemainingMs or 0) / 1000,
remainingMs = cd.cooldownRemainingMs or 0,
gcdRemaining = (cd.gcdCategoryRemainingMs or 0) / 1000,
gcdRemainingMs = cd.gcdCategoryRemainingMs or 0,
individualRemaining = (cd.individualRemainingMs or 0) / 1000,
categoryRemaining = (cd.categoryRemainingMs or 0) / 1000,
}
end
-- Item cooldown helper
pfUI.api.GetPreciseItemCooldown = function(itemId)
if not GetItemIdCooldown then return nil end
local cd = GetItemIdCooldown(itemId)
if not cd then return nil end
return {
onCooldown = (cd.isOnCooldown or 0) == 1,
remaining = (cd.cooldownRemainingMs or 0) / 1000,
remainingMs = cd.cooldownRemainingMs or 0,
}
end
end
-- UNIT_DIED event handling - placeholder for future use
-- (Debuff/buff cleanup removed as tracking is now handled by libdebuff)
-- Trinket Management API
if GetTrinkets then
pfUI.api.GetEquippedTrinkets = function()
local trinkets = GetTrinkets()
if not trinkets then return {} end
local equipped = {}
for _, trinket in pairs(trinkets) do
if trinket and trinket.bagIndex == nil then -- nil bagIndex = equipped
table.insert(equipped, trinket)
end
end
return equipped
end
pfUI.api.GetTrinketCooldown = function(slot)
if not GetTrinketCooldown then return nil end
local cd = GetTrinketCooldown(slot)
if cd == -1 or not cd then return nil end
return {
onCooldown = (cd.isOnCooldown or 0) == 1,
remaining = (cd.cooldownRemainingMs or 0) / 1000,
remainingMs = cd.cooldownRemainingMs or 0,
}
end
pfUI.api.UseTrinket = function(slot, target)
if not UseTrinket then return false end
return UseTrinket(slot, target) == 1
end
end
-- Nampower Item Stats API (use distinct name to avoid conflicts)
if GetItemStats then
pfUI.api.GetNampowerItemStats = function(itemId)
local success, stats = pcall(GetItemStats, itemId, true)
if not success or not stats then return nil end
return stats
end
-- Quick item level lookup
pfUI.api.GetNampowerItemLevel = function(itemId)
if GetItemLevel then
return GetItemLevel(itemId)
end
local success, stats = pcall(GetItemStats, itemId, true)
if success and stats and stats.itemLevel then
return stats.itemLevel
end
return nil
end
end
-- Spell Modifiers API for damage/heal predictions
if GetSpellModifiers then
pfUI.api.GetSpellBonus = function(spellId, modType)
-- modType: 0=DAMAGE, 1=DURATION, 6=RADIUS, 7=CRIT, 10=CAST_TIME, 14=COST, etc.
local flat, percent, hasmod = GetSpellModifiers(spellId, modType or 0)
return {
flat = flat or 0,
percent = percent or 0,
hasModifier = hasmod and hasmod ~= 0,
}
end
-- Common spell modifier lookups
pfUI.api.GetSpellDamageBonus = function(spellId)
return pfUI.api.GetSpellBonus(spellId, 0) -- DAMAGE
end
pfUI.api.GetSpellCritBonus = function(spellId)
return pfUI.api.GetSpellBonus(spellId, 7) -- CRITICAL_CHANCE
end
pfUI.api.GetSpellCostReduction = function(spellId)
return pfUI.api.GetSpellBonus(spellId, 14) -- COST
end
end
-- Inventory/Bag API
if GetBagItems then
pfUI.api.GetAllBagItems = function()
return GetBagItems()
end
pfUI.api.FindItem = function(itemIdOrName)
if FindPlayerItemSlot then
local bag, slot = FindPlayerItemSlot(itemIdOrName)
return bag, slot
end
return nil, nil
end
pfUI.api.UseItem = function(itemIdOrName, target)
if UseItemIdOrName then
return UseItemIdOrName(itemIdOrName, target) == 1
end
return false
end
end
-- Equipment Inspection API
if GetEquippedItems then
pfUI.api.GetPlayerEquipment = function()
return GetEquippedItems("player")
end
pfUI.api.GetTargetEquipment = function()
return GetEquippedItems("target")
end
pfUI.api.GetEquippedItemInfo = function(unit, slot)
if GetEquippedItem then
return GetEquippedItem(unit, slot)
end
return nil
end
end
-- Spell Lookup Helpers
if GetSpellIdForName then
pfUI.api.GetMaxRankSpellId = function(spellName)
return GetSpellIdForName(spellName)
end
end
if GetSpellSlotTypeIdForName then
pfUI.api.GetSpellSlotInfo = function(spellName)
local slot, bookType, spellId = GetSpellSlotTypeIdForName(spellName)
return {
slot = slot,
bookType = bookType,
spellId = spellId,
}
end
end
-- Queue Script API for advanced macro functionality
if QueueScript then
pfUI.api.QueueLuaScript = function(script, priority)
QueueScript(script, priority or 1)
end
end
if QueueSpellByName then
pfUI.api.QueueSpell = function(spellName)
QueueSpellByName(spellName)
end
end
-- Channel optimization
if ChannelStopCastingNextTick then
pfUI.api.StopChannelNextTick = function()
ChannelStopCastingNextTick()
end
end
-- Spell Database Access via GetSpellRec
if GetSpellRec then
pfUI.api.GetSpellRecord = function(spellId)
local success, rec = pcall(GetSpellRec, spellId)
if not success or not rec then return nil end
return {
spellId = spellId,
name = rec.name or "",
rank = rec.rank or "",
description = rec.description or "",
manaCost = rec.manaCost or 0,
baseLevel = rec.baseLevel or 0,
spellLevel = rec.spellLevel or 0,
maxLevel = rec.maxLevel or 0,
maxTargetLevel = rec.maxTargetLevel or 0,
maxTargets = rec.maxTargets or 0,
durationIndex = rec.durationIndex or 0,
powerType = rec.powerType or 0,
rangeIndex = rec.rangeIndex or 0,
speed = rec.speed or 0,
schoolMask = rec.schoolMask or 0,
runeCostID = rec.runeCostID or 0,
spellMissileID = rec.spellMissileID or 0,
iconID = rec.iconID or 0,
activeIconID = rec.activeIconID or 0,
nameSubtext = rec.nameSubtext or "",
castingTimeIndex = rec.castingTimeIndex or 0,
categoryRecoveryTime = rec.categoryRecoveryTime or 0,
recoveryTime = rec.recoveryTime or 0,
startRecoveryCategory = rec.startRecoveryCategory or 0,
startRecoveryTime = rec.startRecoveryTime or 0,
}
end
-- Get spell school (fire, frost, nature, etc.)
pfUI.api.GetSpellSchool = function(spellId)
local success, rec = pcall(GetSpellRec, spellId)
if not success or not rec or not rec.schoolMask then return nil end
local schools = {
[1] = "Physical",
[2] = "Holy",
[4] = "Fire",
[8] = "Nature",
[16] = "Frost",
[32] = "Shadow",
[64] = "Arcane",
}
return schools[rec.schoolMask] or "Unknown"
end
end
-- Disenchant All utility
if DisenchantAll then
pfUI.api.DisenchantAllItems = function()
DisenchantAll()
end
SLASH_PFDISENCHANTALL1 = "/disenchantall"
SLASH_PFDISENCHANTALL2 = "/dea"
SlashCmdList["PFDISENCHANTALL"] = function()
DisenchantAll()
DEFAULT_CHAT_FRAME:AddMessage("|cff33ffccpfUI|r: Disenchanting all eligible items...")
end
end
-- Druid Secondary Mana Bar
-- Shows base mana when druid is in shapeshift form (Bear/Cat uses Rage/Energy)
-- Uses Nampower's GetUnitField to get base mana values
-- Fully self-contained: uses its own config settings from C.unitframes.druidmana*
local _, playerClass = UnitClass("player")
if GetUnitField and pfUI.uf and playerClass == "DRUID" and pfUI_config.unitframes.druidmanabar == "1" then
local rawborder, default_border = GetBorderSize("unitframes")
local DC = C.unitframes -- druid mana config lives here as druidmana* keys
-- Shared helper: create a druid mana bar on a unit frame
local function CreateDruidManaBar(parent, unit)
if not parent then return nil end
local parentConfig = parent.config
-- Read own config values
local dmHeight = tonumber(DC.druidmanaheight) or 10
local dmWidth = DC.druidmanawidth or "-1"
local dmOffX = tonumber(DC.druidmanaoffx) or 0
local dmOffY = tonumber(DC.druidmanaoffy) or 0
local dmSpace = tonumber(DC.druidmanaspace) or -3
local dmTexture = DC.druidmanatexture or "Interface\\AddOns\\pfUI\\img\\bar"
local bar = CreateFrame("StatusBar", "pfDruidMana_" .. unit, parent)
bar:SetFrameStrata(parent:GetFrameStrata())
bar:SetFrameLevel(parent:GetFrameLevel() + 5)
bar:SetStatusBarTexture(pfUI.media[dmTexture] or dmTexture)
-- Bar color: use same manacolor logic as the normal power bar
local manacolor = parentConfig.defcolor == "0" and parentConfig.manacolor or C.unitframes.manacolor
local r, g, b, a = pfUI.api.strsplit(",", manacolor)
bar:SetStatusBarColor(tonumber(r) or .25, tonumber(g) or .25, tonumber(b) or 1, tonumber(a) or 1)
-- Size: own width/height, fallback to parent power bar width if -1
local width = dmWidth ~= "-1" and tonumber(dmWidth) or nil
if width then
bar:SetWidth(width)
end
bar:SetHeight(dmHeight)
-- Position below the power bar with own spacing + offsets
local spacing = -2 * default_border - dmSpace
if width then
-- Fixed width: use single point with offset
bar:SetPoint("TOP", parent.power, "BOTTOM", dmOffX, spacing + dmOffY)
else
-- Auto width: anchor to both sides of power bar
bar:SetPoint("TOPLEFT", parent.power, "BOTTOMLEFT", dmOffX, spacing + dmOffY)
bar:SetPoint("TOPRIGHT", parent.power, "BOTTOMRIGHT", dmOffX, spacing + dmOffY)
end
bar:Hide()
CreateBackdrop(bar)
CreateBackdropShadow(bar)
-- Font settings (same logic as power bar)
local fontname = pfUI.font_unit
local fontsize = tonumber(pfUI_config.global.font_unit_size)
local fontstyle = pfUI_config.global.font_unit_style
if parentConfig.customfont == "1" then
fontname = pfUI.media[parentConfig.customfont_name]
fontsize = tonumber(parentConfig.customfont_size)
fontstyle = parentConfig.customfont_style
end
-- Text color (always mana-colored)
local tr, tg, tb = ManaBarColor[0].r, ManaBarColor[0].g, ManaBarColor[0].b
if C.unitframes.pastel == "1" then
tr, tg, tb = (tr + .75) * .5, (tg + .75) * .5, (tb + .75) * .5
end
-- Single center text showing current/max
bar.text = bar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
bar.text:SetFontObject(GameFontWhite)
bar.text:SetFont(fontname, fontsize, fontstyle)
bar.text:SetPoint("CENTER", bar, "CENTER", 0, 0)
bar.text:SetJustifyH("CENTER")
bar.text:SetTextColor(tr, tg, tb, 1)
return bar
end
-- Shared helper: update druid mana bar values and text
local function UpdateDruidManaBar(bar, unit)
if not UnitExists(unit) then
bar:Hide()
return
end
local powerType = UnitPowerType(unit)
-- Only show when NOT using mana (i.e., in Bear/Cat form)
if powerType == 0 then
bar:Hide()
return
end
-- Get base mana using Nampower's GetUnitField
local baseMana, baseMaxMana
local _, guid = UnitExists(unit)
if guid then
baseMana = GetUnitField(guid, "power1")
baseMaxMana = GetUnitField(guid, "maxPower1")
end
-- Round down power values (Nampower can return decimals)
if baseMana then baseMana = math.floor(baseMana) end
if baseMaxMana then baseMaxMana = math.floor(baseMaxMana) end
if type(baseMana) ~= "number" or type(baseMaxMana) ~= "number" or baseMaxMana == 0 then
bar:Hide()
return
end
-- Update bar
bar:SetMinMaxValues(0, baseMaxMana)
bar:SetValue(baseMana)
-- Always show current/max
bar.text:SetText(string.format("%s/%s", Abbreviate(baseMana), Abbreviate(baseMaxMana)))
bar:Show()
end
-- ===== Player Druid Mana Bar =====
if pfUI.uf.player then
local playerMana = CreateDruidManaBar(pfUI.uf.player, "player")
if playerMana then
playerMana:RegisterEvent("UNIT_MANA")
playerMana:RegisterEvent("UNIT_MAXMANA")
playerMana:RegisterEvent("UNIT_DISPLAYPOWER")
playerMana:RegisterEvent("UPDATE_SHAPESHIFT_FORM")
playerMana:RegisterEvent("PLAYER_LOGOUT")
playerMana:SetScript("OnEvent", function()
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
return
end
if arg1 == nil or arg1 == "player" then
UpdateDruidManaBar(playerMana, "player")
end
end)
-- Initial update
UpdateDruidManaBar(playerMana, "player")
end
end
-- ===== Target Druid Mana Bar =====
if pfUI.uf.target then
local targetMana = CreateDruidManaBar(pfUI.uf.target, "target")
if targetMana then
targetMana:RegisterEvent("UNIT_MANA")
targetMana:RegisterEvent("UNIT_MAXMANA")
targetMana:RegisterEvent("UNIT_DISPLAYPOWER")
targetMana:RegisterEvent("PLAYER_TARGET_CHANGED")
targetMana:RegisterEvent("PLAYER_LOGOUT")
targetMana:SetScript("OnEvent", function()
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
return
end
if event == "PLAYER_TARGET_CHANGED" or arg1 == nil or arg1 == "target" then
UpdateDruidManaBar(targetMana, "target")
end
end)
-- Initial update
UpdateDruidManaBar(targetMana, "target")
end
end
end
end)
+6 -1
View File
@@ -11,6 +11,11 @@ pfUI:RegisterModule("player", "vanilla:tbc", function ()
pfUI.uf.player:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOM", -75, 125)
UpdateMovable(pfUI.uf.player)
-- Add throttle to player frame OnUpdate
if pfUI.uf.player:GetScript("OnUpdate") then
pfUI.uf.player:SetScript("OnUpdate", pfUI.uf.player:GetScript("OnUpdate"))
end
-- Replace default's RESET_INSTANCES button with an always working one
UnitPopupButtons["RESET_INSTANCES_FIX"] = { text = RESET_INSTANCES, dist = 0 }
for id, text in pairs(UnitPopupMenus["SELF"]) do
@@ -25,4 +30,4 @@ pfUI:RegisterModule("player", "vanilla:tbc", function ()
StaticPopup_Show("CONFIRM_RESET_INSTANCES")
end
end)
end)
end)
+31 -1
View File
@@ -87,6 +87,10 @@ pfUI:RegisterModule("raid", "vanilla:tbc", function ()
pfUI.uf.raid:RegisterEvent("VARIABLES_LOADED")
pfUI.uf.raid:SetScript("OnEvent", function() this:Show() end)
pfUI.uf.raid:SetScript("OnUpdate", function()
-- Throttle raid roster updates to 1 FPS
if (this.tick or 0) > GetTime() then return end
this.tick = GetTime() + 1.0
-- don't proceed without raid or during combat
if not UnitInRaid("player") or (InCombatLockdown and InCombatLockdown()) then return end
@@ -109,6 +113,32 @@ pfUI:RegisterModule("raid", "vanilla:tbc", function ()
end
end
-- Smart GUID-based updates: only refresh frames where unit changed
if pfUI.uf.guidTracker then
local tracker = pfUI.uf.guidTracker
for i = 1, maxraid do
local frame = pfUI.uf.raid[i]
if frame and frame.id and frame.id > 0 then
local unit = "raid" .. frame.id
local _, newGuid = UnitExists(unit)
local oldGuid = tracker.frameToGuid[frame]
if newGuid ~= oldGuid then
-- GUID changed = different player = need full update
tracker.frameToGuid[frame] = newGuid
frame.update_full = true
frame.update_aura = true -- Force aura refresh!
end
end
end
end
-- rebuild unitmap after frame IDs are assigned
if pfUI.uf.RebuildUnitmap then
pfUI.uf.RebuildUnitmap()
end
this:Hide()
end)
@@ -131,4 +161,4 @@ pfUI:RegisterModule("raid", "vanilla:tbc", function ()
pfUI.uf.raid:Show()
end
end)
end)
end)
+5 -2
View File
@@ -1,7 +1,10 @@
pfUI:RegisterModule("skin", "vanilla:tbc", function ()
pfUI:RegisterModule("skin", "vanilla", function ()
-- align UIParent panels
pfUI.panelalign = CreateFrame("Frame", "pfUIParentPanelAlign", UIParent)
pfUI.panelalign:SetScript("OnUpdate", function()
-- throttle to 5 updates per second instead of every frame
if (this.tick or 0.2) > GetTime() then return else this.tick = GetTime() + 0.2 end
local left = UIParent.left
local center = UIParent.center
local rbpos, ropos
@@ -76,4 +79,4 @@ pfUI:RegisterModule("skin", "vanilla:tbc", function ()
else
UIErrorsFrame:RegisterEvent("UI_ERROR_MESSAGE")
end
end)
end)
+245 -215
View File
@@ -1,6 +1,43 @@
-- Compatibility layer to use castbars provided by SuperWoW:
-- https://github.com/balakethelock/SuperWoW
-- DLL Status Check Command (always available)
SLASH_PFDLLSTATUS1 = "/pfdll"
SlashCmdList["PFDLLSTATUS"] = function()
local chat = DEFAULT_CHAT_FRAME
chat:AddMessage("|cff33ffccpfUI|r: DLL Status Check")
-- SuperWoW
if SUPERWOW_VERSION then
chat:AddMessage(" |cff00ff00SuperWoW|r: v" .. tostring(SUPERWOW_VERSION))
elseif SpellInfo or SetAutoloot then
chat:AddMessage(" |cffffff00SuperWoW|r: Detected (old version)")
else
chat:AddMessage(" |cffff0000SuperWoW|r: Not detected")
end
-- Nampower
if GetNampowerVersion then
chat:AddMessage(" |cff00ff00Nampower|r: v" .. tostring(GetNampowerVersion()))
else
chat:AddMessage(" |cffff0000Nampower|r: Not detected")
end
-- Check if castbar exists for indicator positioning
if pfUI.castbar and pfUI.castbar.player then
chat:AddMessage(" |cff00ff00Castbar|r: Available for indicator anchoring")
else
chat:AddMessage(" |cffffff00Castbar|r: Not available (indicators use fallback position)")
end
-- Check indicator frames
if pfUI.uf and pfUI.uf.target then
chat:AddMessage(" |cff00ff00Target frame|r: exists")
else
chat:AddMessage(" |cffff0000Target frame|r: NOT found")
end
end
pfUI:RegisterModule("superwow", "vanilla", function ()
if SetAutoloot and SpellInfo and not SUPERWOW_VERSION then
-- Turn every enchanting link that we create in the enchanting frame,
@@ -46,214 +83,201 @@ pfUI:RegisterModule("superwow", "vanilla", function ()
end)
end
-- Add native mouseover support
if SUPERWOW_VERSION and pfUI.uf and pfUI.uf.mouseover then
_G.SlashCmdList.PFCAST = function(msg)
local func = loadstring(msg or "")
local unit = "mouseover"
-- TrackUnit API for adding group members to minimap
-- Tracks friendly units on the minimap for easier group coordination
if TrackUnit and C.unitframes.track_group == "1" then
local trackFrame = CreateFrame("Frame")
trackFrame:RegisterEvent("PARTY_MEMBERS_CHANGED")
trackFrame:RegisterEvent("RAID_ROSTER_UPDATE")
trackFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
trackFrame:RegisterEvent("PLAYER_LOGOUT")
if not UnitExists(unit) then
local frame = GetMouseFocus()
if frame.label and frame.id then
unit = frame.label .. frame.id
elseif UnitExists("target") then
unit = "target"
elseif GetCVar("autoSelfCast") == "1" then
unit = "player"
else
return
end
end
if func then
-- set mouseover to target for script if needed
local switch_target = not UnitIsUnit("target", unit)
if switch_target then TargetUnit(unit) end
func()
if switch_target then TargetLastTarget() end
else
-- write temporary unit name
pfUI.uf.mouseover.unit = unit
-- cast spell to unitstr
CastSpellByName(msg, unit)
-- remove temporary mouseover unit
pfUI.uf.mouseover.unit = nil
end
end
end
-- Add support for druid mana bars
if SUPERWOW_VERSION and pfUI.uf and pfUI.uf.player and pfUI_config.unitframes.druidmanabar == "1" then
local parent = pfUI.uf.player.power.bar
local config = pfUI.uf.player.config
local mana = config.defcolor == "0" and config.manacolor or pfUI_config.unitframes.manacolor
local r, g, b, a = pfUI.api.strsplit(",", mana)
local rawborder, default_border = GetBorderSize("unitframes")
local _, class = UnitClass("player")
local width = config.pwidth ~= "-1" and config.pwidth or config.width
local fontname = pfUI.font_unit
local fontsize = tonumber(pfUI_config.global.font_unit_size)
local fontstyle = pfUI_config.global.font_unit_style
if config.customfont == "1" then
fontname = pfUI.media[config.customfont_name]
fontsize = tonumber(config.customfont_size)
fontstyle = config.customfont_style
end
local druidmana = CreateFrame("StatusBar", "pfDruidMana", UIParent)
druidmana:SetFrameStrata(parent:GetFrameStrata())
druidmana:SetFrameLevel(parent:GetFrameLevel() + 16)
druidmana:SetStatusBarTexture(pfUI.media[config.pbartexture])
druidmana:SetStatusBarColor(r, g, b, a)
druidmana:SetPoint("TOPLEFT", parent, "BOTTOMLEFT", 0, -2*default_border - config.pspace)
druidmana:SetPoint("TOPRIGHT", parent, "BOTTOMRIGHT", 0, -2*default_border - config.pspace)
druidmana:SetWidth(width)
druidmana:SetHeight(tonumber(pfUI_config.unitframes.druidmanaheight) or 6)
druidmana:EnableMouse(true)
druidmana:Hide()
UpdateMovable(druidmana)
CreateBackdrop(druidmana)
CreateBackdropShadow(druidmana)
druidmana:RegisterEvent("UNIT_MANA")
druidmana:RegisterEvent("UNIT_MAXMANA")
druidmana:RegisterEvent("UNIT_DISPLAYPOWER")
druidmana:SetScript("OnEvent", function()
if UnitPowerType("player") == 0 then
this:Hide()
trackFrame:SetScript("OnEvent", function()
-- Handle shutdown to prevent crash 132
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
return
end
local _, mana = UnitMana("player")
local _, max = UnitManaMax("player")
local perc = math.ceil(mana / max * 100)
if perc == 100 then
this.text:SetText(string.format("%s", Abbreviate(mana)))
else
this.text:SetText(string.format("%s - %s%%", Abbreviate(mana), perc))
-- Track party members
for i = 1, 4 do
local unit = "party" .. i
if UnitExists(unit) and UnitIsConnected(unit) then
pcall(TrackUnit, unit)
end
end
-- Track raid members
for i = 1, 40 do
local unit = "raid" .. i
if UnitExists(unit) and UnitIsConnected(unit) and not UnitIsUnit(unit, "player") then
pcall(TrackUnit, unit)
end
end
this:SetMinMaxValues(0, max)
this:SetValue(mana)
this:Show()
end)
end
druidmana.text = druidmana:CreateFontString("Status", "OVERLAY", "GameFontNormalSmall")
druidmana.text:SetFontObject(GameFontWhite)
druidmana.text:SetFont(fontname, fontsize, fontstyle)
druidmana.text:SetPoint("RIGHT", -2*(default_border + config.txtpowerrightoffx), 0)
druidmana.text:SetPoint("LEFT", 2*(default_border + config.txtpowerrightoffx), 0)
druidmana.text:SetJustifyH("RIGHT")
-- Raid Marker Targeting API
-- Allows targeting units by raid marker ("mark1" to "mark8")
if SUPERWOW_VERSION then
pfUI.api.GetMarkedUnit = function(markIndex)
local markUnit = "mark" .. markIndex
if UnitExists(markUnit) then
return markUnit
end
return nil
end
if config["powercolor"] == "1" then
local r = ManaBarColor[0].r
local g = ManaBarColor[0].g
local b = ManaBarColor[0].b
pfUI.api.TargetMark = function(markIndex)
local markUnit = "mark" .. markIndex
if UnitExists(markUnit) then
TargetUnit(markUnit)
return true
end
return false
end
if pfUI_config.unitframes.pastel == "1" then
druidmana.text:SetTextColor((r+.75)*.5, (g+.75)*.5, (b+.75)*.5, 1)
-- Get owner of pet/totem using "owner" suffix
pfUI.api.GetUnitOwner = function(unit)
local ownerUnit = unit .. "owner"
if UnitExists(ownerUnit) then
return UnitName(ownerUnit), ownerUnit
end
return nil
end
end
-- Clickthrough Mode API
-- Allows clicking through corpses to loot underneath
if Clickthrough then
pfUI.api.SetClickthrough = function(enabled)
Clickthrough(enabled and 1 or 0)
end
pfUI.api.GetClickthrough = function()
return Clickthrough() == 1
end
pfUI.api.ToggleClickthrough = function()
local current = Clickthrough()
Clickthrough(current == 1 and 0 or 1)
return Clickthrough() == 1
end
-- Add slash command for clickthrough toggle
SLASH_PFCLICKTHROUGH1 = "/clickthrough"
SLASH_PFCLICKTHROUGH2 = "/ct"
SlashCmdList["PFCLICKTHROUGH"] = function()
local enabled = pfUI.api.ToggleClickthrough()
DEFAULT_CHAT_FRAME:AddMessage("|cff33ffccpfUI|r: Clickthrough mode " .. (enabled and "|cff00ff00enabled|r" or "|cffff0000disabled|r"))
end
end
-- Autoloot Control API
if SetAutoloot then
pfUI.api.SetAutoloot = function(enabled)
SetAutoloot(enabled and 1 or 0)
end
pfUI.api.GetAutoloot = function()
return SetAutoloot() == 1
end
pfUI.api.ToggleAutoloot = function()
local current = SetAutoloot()
SetAutoloot(current == 1 and 0 or 1)
return SetAutoloot() == 1
end
end
-- GetPlayerBuffID wrapper
if GetPlayerBuffID then
pfUI.api.GetPlayerBuffSpellId = function(buffIndex)
return GetPlayerBuffID(buffIndex)
end
end
-- CombatLogAdd wrapper for logging
if CombatLogAdd then
pfUI.api.LogToCombatLog = function(text, raw)
CombatLogAdd(text, raw and 1 or nil)
end
end
-- Local Raid Markers (marks only visible to self)
if SetRaidTarget then
local origSetRaidTarget = SetRaidTarget
pfUI.api.SetLocalRaidTarget = function(unit, index)
origSetRaidTarget(unit, index, "local")
end
end
-- Enhanced GetContainerItemInfo for charges
-- SuperWoW returns charges as negative numbers
pfUI.api.GetItemCharges = function(bag, slot)
local texture, count = GetContainerItemInfo(bag, slot)
if count and count < 0 then
return math.abs(count) -- Return positive charge count
end
return nil -- Not a charged item
end
-- Weapon Enchant Info on other players
if GetWeaponEnchantInfo then
local origGetWeaponEnchantInfo = GetWeaponEnchantInfo
pfUI.api.GetUnitWeaponEnchants = function(unit)
if unit and unit ~= "player" then
local mhName, ohName = GetWeaponEnchantInfo(unit)
return {
mainHand = mhName,
offHand = ohName,
}
else
druidmana.text:SetTextColor(r, g, b, a)
local hasMainHandEnchant, mainHandExpiration, mainHandCharges, hasOffHandEnchant, offHandExpiration, offHandCharges = origGetWeaponEnchantInfo()
return {
mainHand = hasMainHandEnchant and true or false,
mainHandExpiration = mainHandExpiration,
mainHandCharges = mainHandCharges,
offHand = hasOffHandEnchant and true or false,
offHandExpiration = offHandExpiration,
offHandCharges = offHandCharges,
}
end
end
if pfUI_config.unitframes.druidmanatext == "1" then
druidmana.text:Show()
else
druidmana.text:Hide()
end
if class ~= "DRUID" then
druidmana:UnregisterAllEvents()
druidmana:Hide()
end
end
-- Add support for guid based focus frame
if SUPERWOW_VERSION and pfUI.uf and pfUI.uf.focus then
local focus = function(unitstr)
-- try to read target's unit guid
local _, guid = UnitExists(unitstr)
if guid and pfUI.uf.focus then
-- update focus frame
pfUI.uf.focus.unitname = nil
pfUI.uf.focus.label = guid
pfUI.uf.focus.id = ""
-- update focustarget frame
pfUI.uf.focustarget.unitname = nil
pfUI.uf.focustarget.label = guid .. "target"
pfUI.uf.focustarget.id = ""
end
return guid
end
-- extend the builtin /focus slash command
local legacyfocus = SlashCmdList.PFFOCUS
function SlashCmdList.PFFOCUS(msg)
-- try to perform guid based focus
local guid = focus("target")
-- run old focus emulation
if not guid then legacyfocus(msg) end
end
-- extend the builtin /swapfocus slash command
local legacyswapfocus = SlashCmdList.PFSWAPFOCUS
function SlashCmdList.PFSWAPFOCUS(msg)
-- save previous focus values
local oldlabel = pfUI.uf.focus.label or ""
local oldid = pfUI.uf.focus.id or ""
-- try to perform guid based focus
local guid = focus("target")
-- target old focus
if guid and oldlabel and oldid then
TargetUnit(oldlabel..oldid)
end
-- run old focus emulation
if not guid then legacyswapfocus(msg) end
end
end
-- Enhance libdebuff with SuperWoW data
local superdebuff = CreateFrame("Frame")
superdebuff:RegisterEvent("UNIT_CASTEVENT")
superdebuff:SetScript("OnEvent", function()
-- variable assignments
local caster, target, event, spell, duration = arg1, arg2, arg3, arg4
-- skip other caster and empty target events
local _, guid = UnitExists("player")
if caster ~= guid then return end
if event ~= "CAST" then return end
if not target or target == "" then return end
-- assign all required data
local unit = UnitName(target)
local unitlevel = UnitLevel(target)
local effect, rank = SpellInfo(spell)
local duration = libdebuff:GetDuration(effect, rank)
local caster = "player"
-- add effect to current debuff data
libdebuff:AddEffect(unit, unitlevel, effect, duration, caster)
end)
-- Enhance libcast with SuperWoW data
-- 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()
if not supercast.init then
-- disable combat parsing events in superwow mode
-- 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 = UnitExists("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")
@@ -276,17 +300,21 @@ pfUI:RegisterModule("superwow", "vanilla", function ()
end
if arg3 == "START" or arg3 == "CAST" or arg3 == "CHANNEL" then
-- human readable argument list
local guid = arg1
local target = arg2
local event_type = arg3
local spell_id = arg4
local timer = arg5
local start = GetTime()
-- get spell info from spell id
local spell, icon, _
if SpellInfo and SpellInfo(spell_id) then
if GetSpellRec then
local rec = GetSpellRec(spell_id)
if rec then
spell = rec.name
local iconID = rec.spellIconID
icon = iconID and GetSpellIconTexture(iconID) or nil
end
elseif SpellInfo and SpellInfo(spell_id) then
spell, _, icon = SpellInfo(spell_id)
end
@@ -304,28 +332,30 @@ pfUI:RegisterModule("superwow", "vanilla", function ()
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[guid] then libcast.db[guid] = {} end
libcast.db[guid].cast = spell
libcast.db[guid].rank = nil
libcast.db[guid].start = GetTime()
libcast.db[guid].casttime = timer
libcast.db[guid].icon = icon
libcast.db[guid].channel = event_type == "CHANNEL" or false
-- write state variable
superwow_active = true
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
local guid = arg1
-- delete all cast entries of guid
if libcast.db[guid] then
libcast.db[guid].cast = nil
libcast.db[guid].rank = nil
libcast.db[guid].start = nil
libcast.db[guid].casttime = nil
libcast.db[guid].icon = nil
libcast.db[guid].channel = nil
-- 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)
+586
View File
@@ -0,0 +1,586 @@
pfUI:RegisterModule("swingtimer", "vanilla:tbc", function ()
local rawborder, border = GetBorderSize()
-- HitInfo flags (EVENTS.md)
local HITINFO_LEFTSWING = 4 -- 0x4: Off-hand attack
local HITINFO_NOACTION = 65536 -- 0x10000: server did not advance the swing clock
-- SPELL_QUEUE_EVENT codes (EVENTS.md)
local ON_SWING_QUEUED = 0
local ON_SWING_QUEUE_POPPED = 1
-- Swing state
local swingState = {
mainhand = { speed = 0, nextSwing = 0, swinging = false },
offhand = { speed = 0, nextSwing = 0, swinging = false },
ranged = { speed = 0, nextSwing = 0, swinging = false },
}
-- Ranged spell IDs that trigger the ranged swing timer (replaces MH)
local RANGED_SPELLIDS = {
[75] = true, -- Auto Shot (Hunter)
[2764] = true, -- Throw (Warrior/Rogue)
}
-- Create container frame
pfUI.swingtimer = CreateFrame("Frame", "pfSwingTimer", UIParent)
pfUI.swingtimer:SetFrameStrata("MEDIUM")
pfUI.swingtimer:Hide()
-- Read config once at load into locals
local sw_width = tonumber(C.unitframes.swingtimerwidth) or 200
local sw_height = tonumber(C.unitframes.swingtimerheight) or 12
local sw_texture = C.unitframes.swingtimertexture or "Interface\\AddOns\\pfUI\\img\\bar"
local sw_showtext = C.unitframes.swingtimertext ~= "0"
local sw_showlabel = C.unitframes.swingtimerlabel ~= "0"
local sw_showoh = C.unitframes.swingtimeroffhand ~= "0"
local sw_showranged = C.unitframes.swingtimerranged ~= "0"
local sw_fontsize = tonumber(C.unitframes.swingtimerfontsize) or 12
local sw_hsqueue = C.unitframes.swingtimerhsqueue ~= "0"
-- Parse color strings "r,g,b,a" into components
local function ParseColor(str, dr, dg, db, da)
if not str or str == "" then return dr, dg, db, da end
local _, _, r, g, b, a = string.find(str, "([%d%.]+),([%d%.]+),([%d%.]+),([%d%.]+)")
if r then
return tonumber(r) or dr, tonumber(g) or dg, tonumber(b) or db, tonumber(a) or da
end
return dr, dg, db, da
end
local mhR, mhG, mhB, mhA = ParseColor(C.unitframes.swingtimermhcolor, 0.8, 0.3, 0.3, 1)
local ohR, ohG, ohB, ohA = ParseColor(C.unitframes.swingtimerohcolor, 0.3, 0.8, 0.3, 1)
local raR, raG, raB, raA = ParseColor(C.unitframes.swingtimerrangedcolor, 0.3, 0.6, 1.0, 1)
local rwR, rwG, rwB, rwA = ParseColor(C.unitframes.swingtimerrangedwarncolor, 0.9, 0.0, 0.0, 1)
local isHunter = UnitClass("player") == "Hunter"
-- Store default MH color for HS/Cleave restore
local mhDefaultR, mhDefaultG, mhDefaultB = mhR, mhG, mhB
-- Mainhand bar
pfUI.swingtimer.mainhand = CreateFrame("StatusBar", "pfSwingTimerMainhand", UIParent)
pfUI.swingtimer.mainhand:SetPoint("CENTER", UIParent, "CENTER", 0, -100)
pfUI.swingtimer.mainhand:SetWidth(sw_width)
pfUI.swingtimer.mainhand:SetHeight(sw_height)
pfUI.swingtimer.mainhand:SetMinMaxValues(0, 1)
pfUI.swingtimer.mainhand:SetValue(0)
pfUI.swingtimer.mainhand:SetStatusBarTexture(sw_texture)
pfUI.swingtimer.mainhand:SetStatusBarColor(mhR, mhG, mhB, mhA)
pfUI.swingtimer.mainhand:Hide()
pfUI.swingtimer.mainhand.text = pfUI.swingtimer.mainhand:CreateFontString("Status", "DIALOG", "GameFontNormal")
pfUI.swingtimer.mainhand.text:SetPoint("CENTER", pfUI.swingtimer.mainhand, "CENTER", 0, 0)
pfUI.swingtimer.mainhand.text:SetFont(pfUI.font_default, sw_fontsize, "OUTLINE")
pfUI.swingtimer.mainhand.text:SetTextColor(1, 1, 1, 1)
pfUI.swingtimer.mainhand.text:SetText("")
if not sw_showtext then pfUI.swingtimer.mainhand.text:Hide() end
pfUI.swingtimer.mainhand.label = pfUI.swingtimer.mainhand:CreateFontString("Status", "DIALOG", "GameFontNormal")
pfUI.swingtimer.mainhand.label:SetPoint("RIGHT", pfUI.swingtimer.mainhand, "LEFT", -4, 0)
pfUI.swingtimer.mainhand.label:SetFont(pfUI.font_default, sw_fontsize, "OUTLINE")
pfUI.swingtimer.mainhand.label:SetTextColor(0.8, 0.8, 0.8, 1)
pfUI.swingtimer.mainhand.label:SetText(sw_showlabel and "MH" or "")
CreateBackdrop(pfUI.swingtimer.mainhand)
CreateBackdropShadow(pfUI.swingtimer.mainhand)
-- Offhand bar
pfUI.swingtimer.offhand = CreateFrame("StatusBar", "pfSwingTimerOffhand", UIParent)
pfUI.swingtimer.offhand:SetPoint("TOP", pfUI.swingtimer.mainhand, "BOTTOM", 0, -4)
pfUI.swingtimer.offhand:SetWidth(sw_width)
pfUI.swingtimer.offhand:SetHeight(sw_height)
pfUI.swingtimer.offhand:SetMinMaxValues(0, 1)
pfUI.swingtimer.offhand:SetValue(0)
pfUI.swingtimer.offhand:SetStatusBarTexture(sw_texture)
pfUI.swingtimer.offhand:SetStatusBarColor(ohR, ohG, ohB, ohA)
pfUI.swingtimer.offhand:Hide()
pfUI.swingtimer.offhand.text = pfUI.swingtimer.offhand:CreateFontString("Status", "DIALOG", "GameFontNormal")
pfUI.swingtimer.offhand.text:SetPoint("CENTER", pfUI.swingtimer.offhand, "CENTER", 0, 0)
pfUI.swingtimer.offhand.text:SetFont(pfUI.font_default, sw_fontsize, "OUTLINE")
pfUI.swingtimer.offhand.text:SetTextColor(1, 1, 1, 1)
pfUI.swingtimer.offhand.text:SetText("")
if not sw_showtext then pfUI.swingtimer.offhand.text:Hide() end
pfUI.swingtimer.offhand.label = pfUI.swingtimer.offhand:CreateFontString("Status", "DIALOG", "GameFontNormal")
pfUI.swingtimer.offhand.label:SetPoint("RIGHT", pfUI.swingtimer.offhand, "LEFT", -4, 0)
pfUI.swingtimer.offhand.label:SetFont(pfUI.font_default, sw_fontsize, "OUTLINE")
pfUI.swingtimer.offhand.label:SetTextColor(0.8, 0.8, 0.8, 1)
pfUI.swingtimer.offhand.label:SetText(sw_showlabel and "OH" or "")
CreateBackdrop(pfUI.swingtimer.offhand)
CreateBackdropShadow(pfUI.swingtimer.offhand)
-- Ranged bar (bow/gun/crossbow - triggered by SPELL_GO_SELF for Auto Shot / Throw)
-- Hunter uses a special "close from outside->in, open inside->out" animation
-- instead of a normal left->right StatusBar fill.
pfUI.swingtimer.ranged = CreateFrame("Frame", "pfSwingTimerRanged", UIParent)
pfUI.swingtimer.ranged:SetPoint("CENTER", UIParent, "CENTER", 0, -120)
pfUI.swingtimer.ranged:SetWidth(sw_width)
pfUI.swingtimer.ranged:SetHeight(sw_height)
pfUI.swingtimer.ranged:Hide()
-- Phase 1: left half, anchored to CENTER (right edge fixed), shrinks leftward = outside->in
pfUI.swingtimer.ranged.left = pfUI.swingtimer.ranged:CreateTexture(nil, "ARTWORK")
pfUI.swingtimer.ranged.left:SetTexture(sw_texture)
pfUI.swingtimer.ranged.left:SetPoint("RIGHT", pfUI.swingtimer.ranged, "CENTER", 0, 0)
pfUI.swingtimer.ranged.left:SetHeight(sw_height)
pfUI.swingtimer.ranged.left:SetWidth(sw_width / 2)
pfUI.swingtimer.ranged.left:SetTexCoord(0, 0.5, 0, 1)
pfUI.swingtimer.ranged.left:SetVertexColor(raR, raG, raB, raA)
-- Phase 1: right half, anchored to CENTER (left edge fixed), shrinks rightward = outside->in
pfUI.swingtimer.ranged.right = pfUI.swingtimer.ranged:CreateTexture(nil, "ARTWORK")
pfUI.swingtimer.ranged.right:SetTexture(sw_texture)
pfUI.swingtimer.ranged.right:SetPoint("LEFT", pfUI.swingtimer.ranged, "CENTER", 0, 0)
pfUI.swingtimer.ranged.right:SetHeight(sw_height)
pfUI.swingtimer.ranged.right:SetWidth(sw_width / 2)
pfUI.swingtimer.ranged.right:SetTexCoord(0.5, 1, 0, 1)
pfUI.swingtimer.ranged.right:SetVertexColor(raR, raG, raB, raA)
-- Phase 2: warning color, anchored CENTER, grows outward
pfUI.swingtimer.ranged.warn = pfUI.swingtimer.ranged:CreateTexture(nil, "ARTWORK")
pfUI.swingtimer.ranged.warn:SetTexture(sw_texture)
pfUI.swingtimer.ranged.warn:SetPoint("CENTER", pfUI.swingtimer.ranged, "CENTER", 0, 0)
pfUI.swingtimer.ranged.warn:SetHeight(sw_height)
pfUI.swingtimer.ranged.warn:SetWidth(1)
pfUI.swingtimer.ranged.warn:SetVertexColor(rwR, rwG, rwB, rwA)
pfUI.swingtimer.ranged.warn:Hide()
pfUI.swingtimer.ranged.text = pfUI.swingtimer.ranged:CreateFontString("Status", "DIALOG", "GameFontNormal")
pfUI.swingtimer.ranged.text:SetPoint("CENTER", pfUI.swingtimer.ranged, "CENTER", 0, 0)
pfUI.swingtimer.ranged.text:SetFont(pfUI.font_default, sw_fontsize, "OUTLINE")
pfUI.swingtimer.ranged.text:SetTextColor(1, 1, 1, 1)
pfUI.swingtimer.ranged.text:SetText("")
if not sw_showtext then pfUI.swingtimer.ranged.text:Hide() end
pfUI.swingtimer.ranged.label = pfUI.swingtimer.ranged:CreateFontString("Status", "DIALOG", "GameFontNormal")
pfUI.swingtimer.ranged.label:SetPoint("RIGHT", pfUI.swingtimer.ranged, "LEFT", -4, 0)
pfUI.swingtimer.ranged.label:SetFont(pfUI.font_default, sw_fontsize, "OUTLINE")
pfUI.swingtimer.ranged.label:SetTextColor(0.8, 0.8, 0.8, 1)
pfUI.swingtimer.ranged.label:SetText(sw_showlabel and "Ra" or "")
CreateBackdrop(pfUI.swingtimer.ranged)
CreateBackdropShadow(pfUI.swingtimer.ranged)
-- HS/Cleave queue state
local hsQueued = false
local cleaveQueued = false
local isWarrior = false
local cachedHSSlots = {}
local cachedCleaveSlots = {}
local useSpellQueueEvent = false
-- Heroic Strike spell IDs (all ranks)
local hsSpellIDs = {
[78] = true, [284] = true, [285] = true, [1608] = true,
[11564] = true, [11565] = true, [11566] = true, [11567] = true,
[25286] = true,
}
-- Cleave spell IDs (all ranks)
local cleaveSpellIDs = {
[845] = true, [7369] = true, [11608] = true, [11609] = true,
[20569] = true,
}
local function RebuildQueueSlotCache()
if not isWarrior or not sw_hsqueue or useSpellQueueEvent then return end
cachedHSSlots = {}
cachedCleaveSlots = {}
for slot = 1, 120 do
local tex = GetActionTexture(slot)
local name = GetActionText(slot)
if tex then
if string.find(tex, "Ability_Rogue_Ambush") then
table.insert(cachedHSSlots, slot)
elseif string.find(tex, "Ability_Warrior_Cleave") then
table.insert(cachedCleaveSlots, slot)
end
end
if name then
local lower = string.lower(name)
if lower == "heroic strike" or lower == "heroicstrike" or lower == "hs" then
table.insert(cachedHSSlots, slot)
elseif lower == "cleave" then
table.insert(cachedCleaveSlots, slot)
end
end
end
end
local function CheckQueuedAction(slotList)
for i = 1, table.getn(slotList) do
if IsCurrentAction(slotList[i]) then return true end
end
return false
end
local function IsHSOrCleaveQueued()
if not sw_hsqueue or not isWarrior then return false, false end
if useSpellQueueEvent then
return hsQueued, cleaveQueued
end
return CheckQueuedAction(cachedHSSlots), CheckQueuedAction(cachedCleaveSlots)
end
UpdateMovable(pfUI.swingtimer.mainhand)
UpdateMovable(pfUI.swingtimer.ranged)
-- VERSION B TEST: HasOffhandWeapon() removed.
-- The original used GetItemInfo() to detect OH weapon type, but GetItemInfo()
-- returns nil on first login before the item cache is populated, causing
-- offhand.speed to stay 0. Now we just read offhandAttackTime directly,
-- same as the old working version.
-- Check offhand slot for an actual weapon. GetItemInfo may return nil on first
-- login (item cache not yet populated), so we return nil in that case to signal
-- "unknown" rather than false, allowing the caller to keep the previous value.
-- inventoryType 13 = INVTYPE_WEAPONOFFHAND, 21 = INVTYPE_WEAPON (one-hand, dual wieldable)
-- Shields = 14, held-in-hand = 23, everything else = no swing
local OH_WEAPON_TYPES = { [13]=true, [21]=true }
local function HasOffhandWeapon()
local l = GetInventoryItemLink("player", 17)
if not l then return false end
local _, _, id = string.find(l, "item:(%d+)")
id = tonumber(id)
if not id then return false end
local s = GetItemStats and GetItemStats(id)
if not s then return false end
return OH_WEAPON_TYPES[s.inventoryType] == true
end
local function UpdateWeaponSpeeds()
if not GetUnitField then return end
local mhSpeed = GetUnitField("player", "baseAttackTime")
local ohSpeed = GetUnitField("player", "offhandAttackTime")
if mhSpeed and mhSpeed > 0 then
swingState.mainhand.speed = mhSpeed / 1000
end
if HasOffhandWeapon() and ohSpeed and ohSpeed > 0 then
swingState.offhand.speed = ohSpeed / 1000
else
swingState.offhand.speed = 0
end
local raSpeed = GetUnitField("player", "rangedAttackTime")
if raSpeed and raSpeed > 0 then
swingState.ranged.speed = raSpeed / 1000
else
swingState.ranged.speed = 0
end
end
local function StartSwing(isOffhand)
local now = GetTime()
-- always refresh speeds to catch haste buffs/debuffs
UpdateWeaponSpeeds()
-- dual-wield guard: if MH swing just started (<100ms ago) and this isn't
-- flagged as offhand, it's likely an OH event with missing flag
if not isOffhand and swingState.offhand.speed > 0 then
local mhAge = now - (swingState.mainhand.nextSwing - swingState.mainhand.speed)
if swingState.mainhand.swinging and mhAge > 0 and mhAge < 0.1 then
isOffhand = true
end
end
if isOffhand and swingState.offhand.speed > 0 then
swingState.offhand.nextSwing = now + swingState.offhand.speed
swingState.offhand.swinging = true
if sw_showoh then pfUI.swingtimer.offhand:Show() end
else
swingState.mainhand.nextSwing = now + swingState.mainhand.speed
swingState.mainhand.swinging = true
pfUI.swingtimer.mainhand:Show()
end
pfUI.swingtimer:Show()
end
local function StartRangedSwing()
if not sw_showranged then return end
UpdateWeaponSpeeds()
if swingState.ranged.speed <= 0 then return end
-- Ranged replaces MH: cancel mainhand swing
swingState.mainhand.swinging = false
pfUI.swingtimer.mainhand:Hide()
swingState.ranged.nextSwing = GetTime() + swingState.ranged.speed
swingState.ranged.swinging = true
if isHunter then
-- Hunter: left/right halves anchored to CENTER, shrink outside->in
pfUI.swingtimer.ranged.left:ClearAllPoints()
pfUI.swingtimer.ranged.left:SetPoint("RIGHT", pfUI.swingtimer.ranged, "CENTER", 0, 0)
pfUI.swingtimer.ranged.left:SetWidth(sw_width / 2)
pfUI.swingtimer.ranged.left:SetTexCoord(0, 0.5, 0, 1)
pfUI.swingtimer.ranged.right:SetWidth(sw_width / 2)
pfUI.swingtimer.ranged.right:SetTexCoord(0.5, 1, 0, 1)
else
-- Non-Hunter: left anchored to TOPLEFT, grows left->right like MH/OH
pfUI.swingtimer.ranged.left:ClearAllPoints()
pfUI.swingtimer.ranged.left:SetPoint("TOPLEFT", pfUI.swingtimer.ranged, "TOPLEFT", 0, 0)
pfUI.swingtimer.ranged.left:SetWidth(0.1)
pfUI.swingtimer.ranged.left:Hide()
pfUI.swingtimer.ranged.left:SetTexCoord(0, 0, 0, 1)
pfUI.swingtimer.ranged.right:SetWidth(0.1)
pfUI.swingtimer.ranged.right:Hide()
end
pfUI.swingtimer.ranged.left:SetVertexColor(raR, raG, raB, raA)
pfUI.swingtimer.ranged.right:SetVertexColor(raR, raG, raB, raA)
pfUI.swingtimer.ranged.warn:SetWidth(1)
pfUI.swingtimer.ranged.warn:Hide()
pfUI.swingtimer.ranged:Show()
pfUI.swingtimer:Show()
end
local swingThrottle = 0
pfUI.swingtimer:SetScript("OnUpdate", function()
swingThrottle = swingThrottle + arg1
if swingThrottle < 0.016 then return end
swingThrottle = 0
local now = GetTime()
local anyActive = false
local curR, curG, curB = mhDefaultR, mhDefaultG, mhDefaultB
if sw_hsqueue and isWarrior then
local hs, cl = IsHSOrCleaveQueued()
if cl then
curR, curG, curB = 0.2, 0.9, 0.2
elseif hs then
curR, curG, curB = 0.9, 0.9, 0.2
end
end
if swingState.mainhand.swinging then
local remaining = swingState.mainhand.nextSwing - now
if remaining <= 0 then
swingState.mainhand.swinging = false
pfUI.swingtimer.mainhand:Hide()
else
local progress = 1 - (remaining / swingState.mainhand.speed)
pfUI.swingtimer.mainhand:SetValue(progress)
pfUI.swingtimer.mainhand:SetStatusBarColor(curR, curG, curB, mhA)
if sw_showtext then
pfUI.swingtimer.mainhand.text:SetText(string.format("%.1f", remaining))
end
anyActive = true
end
end
if sw_showoh and swingState.offhand.swinging then
local remaining = swingState.offhand.nextSwing - now
if remaining <= 0 then
swingState.offhand.swinging = false
pfUI.swingtimer.offhand:Hide()
else
local progress = 1 - (remaining / swingState.offhand.speed)
pfUI.swingtimer.offhand:SetValue(progress)
if sw_showtext then
pfUI.swingtimer.offhand.text:SetText(string.format("%.1f", remaining))
end
anyActive = true
end
elseif not sw_showoh then
pfUI.swingtimer.offhand:Hide()
end
if sw_showranged and swingState.ranged.swinging then
local remaining = swingState.ranged.nextSwing - now
if remaining <= 0 then
swingState.ranged.swinging = false
pfUI.swingtimer.ranged:Hide()
else
if isHunter then
-- Hunter ranged animation: two phases
-- Phase 1 (speed-0.5s): bar visible full, shrinks from outside->in toward center (normal color)
-- Phase 2 (0.5s): bar grows from center outward (warning color)
local DEADZONE = 0.5
local halfW = sw_width / 2
if remaining > DEADZONE then
-- Phase 1: left/right halves shrink from outside->in toward center
local elapsed = swingState.ranged.speed - remaining
local phase1dur = swingState.ranged.speed - DEADZONE
local p = elapsed / phase1dur -- 0 = full, 1 = gone
local w = halfW * (1 - p)
if w < 1 then w = 1 end
pfUI.swingtimer.ranged.left:Show()
pfUI.swingtimer.ranged.left:SetWidth(w)
pfUI.swingtimer.ranged.left:SetTexCoord(0, (1 - p) * 0.5, 0, 1)
pfUI.swingtimer.ranged.left:SetVertexColor(raR, raG, raB, raA)
pfUI.swingtimer.ranged.right:Show()
pfUI.swingtimer.ranged.right:SetWidth(w)
pfUI.swingtimer.ranged.right:SetTexCoord(1 - (1 - p) * 0.5, 1, 0, 1)
pfUI.swingtimer.ranged.right:SetVertexColor(raR, raG, raB, raA)
pfUI.swingtimer.ranged.warn:Hide()
else
-- Phase 2: warning color grows from center->outside
local p = 1 - (remaining / DEADZONE) -- 0 = nothing, 1 = full
local w = sw_width * p
if w < 1 then w = 1 end
pfUI.swingtimer.ranged.left:Hide()
pfUI.swingtimer.ranged.right:Hide()
pfUI.swingtimer.ranged.warn:SetWidth(w)
pfUI.swingtimer.ranged.warn:Show()
end
else
-- Non-Hunter (Warrior Throw, Rogue): simple left->right fill like MH/OH
local progress = 1 - (remaining / swingState.ranged.speed)
local w = sw_width * progress
if w < 1 then w = 1 end
pfUI.swingtimer.ranged.left:Show()
pfUI.swingtimer.ranged.left:SetWidth(w)
pfUI.swingtimer.ranged.left:SetTexCoord(0, progress, 0, 1)
pfUI.swingtimer.ranged.right:Hide()
pfUI.swingtimer.ranged.warn:Hide()
end
if sw_showtext then
if isHunter and remaining <= 0.5 then
pfUI.swingtimer.ranged.text:SetText(string.format("%.1f", remaining))
elseif isHunter then
-- Show time until deadzone starts, not full remaining
pfUI.swingtimer.ranged.text:SetText(string.format("%.1f", remaining - 0.5))
else
pfUI.swingtimer.ranged.text:SetText(string.format("%.1f", remaining))
end
end
anyActive = true
end
elseif not sw_showranged then
pfUI.swingtimer.ranged:Hide()
end
if not anyActive then
if not pfUI.swingtimer.mainhand:IsShown()
and not pfUI.swingtimer.offhand:IsShown()
and not pfUI.swingtimer.ranged:IsShown() then
this:Hide()
end
end
end)
local events = CreateFrame("Frame")
events:RegisterEvent("AUTO_ATTACK_SELF")
events:RegisterEvent("AUTO_ATTACK_OTHER")
events:RegisterEvent("PLAYER_ENTERING_WORLD")
events:RegisterEvent("UNIT_INVENTORY_CHANGED")
events:RegisterEvent("PLAYER_REGEN_DISABLED")
events:RegisterEvent("PLAYER_REGEN_ENABLED")
events:RegisterEvent("ACTIONBAR_SLOT_CHANGED")
events:RegisterEvent("UNIT_DIED")
events:RegisterEvent("SPELL_QUEUE_EVENT")
events:RegisterEvent("SPELL_GO_SELF")
local function ResetSwingTimers()
swingState.mainhand.swinging = false
swingState.offhand.swinging = false
swingState.ranged.swinging = false
pfUI.swingtimer.mainhand:Hide()
pfUI.swingtimer.offhand:Hide()
pfUI.swingtimer.ranged:Hide()
pfUI.swingtimer:Hide()
end
local playerGUID = nil
events:SetScript("OnEvent", function()
if event == "AUTO_ATTACK_SELF" then
local hitInfo = arg4 or 0
-- HITINFO_NOACTION: server did not advance the swing clock, ignore
if bit.band(hitInfo, HITINFO_NOACTION) ~= 0 then return end
local isOffhand = bit.band(hitInfo, HITINFO_LEFTSWING) ~= 0
StartSwing(isOffhand)
elseif event == "AUTO_ATTACK_OTHER" then
if not swingState.mainhand.swinging then return end
local targetGuid = arg2
if not targetGuid or not playerGUID then return end
if targetGuid ~= playerGUID then return end
local victimState = arg5 or 0
if victimState == 3 then
local now = GetTime()
local remaining = swingState.mainhand.nextSwing - now
local reduction = swingState.mainhand.speed * 0.4
local minRemaining = swingState.mainhand.speed * 0.2
local newRemaining = remaining - reduction
if newRemaining < minRemaining then newRemaining = minRemaining end
if newRemaining < remaining then
swingState.mainhand.nextSwing = now + newRemaining
end
end
elseif event == "SPELL_QUEUE_EVENT" then
local eventCode = arg1 or -1
local spellId = arg2 or 0
if eventCode == ON_SWING_QUEUED then
useSpellQueueEvent = true
if hsSpellIDs[spellId] then
hsQueued = true; cleaveQueued = false
elseif cleaveSpellIDs[spellId] then
cleaveQueued = true; hsQueued = false
end
elseif eventCode == ON_SWING_QUEUE_POPPED then
hsQueued = false; cleaveQueued = false
end
elseif event == "PLAYER_ENTERING_WORLD" then
local _, class = UnitClass("player")
isWarrior = (class == "WARRIOR")
local _, guid = UnitExists("player")
playerGUID = guid
UpdateWeaponSpeeds()
RebuildQueueSlotCache()
elseif event == "SPELL_GO_SELF" then
local spellId = arg2 or 0
if RANGED_SPELLIDS[spellId] then
StartRangedSwing()
end
elseif event == "UNIT_INVENTORY_CHANGED" then
if arg1 and arg1 ~= "player" then return end
UpdateWeaponSpeeds()
if swingState.offhand.speed == 0 then
swingState.offhand.swinging = false
pfUI.swingtimer.offhand:Hide()
end
if swingState.ranged.speed == 0 then
swingState.ranged.swinging = false
pfUI.swingtimer.ranged:Hide()
end
elseif event == "ACTIONBAR_SLOT_CHANGED" then
RebuildQueueSlotCache()
elseif event == "PLAYER_REGEN_DISABLED" then
UpdateWeaponSpeeds()
elseif event == "PLAYER_REGEN_ENABLED" then
ResetSwingTimers()
hsQueued = false
cleaveQueued = false
elseif event == "UNIT_DIED" then
-- Only reset if the player themselves died
local guid = arg1
if not guid then return end
if guid == playerGUID then
ResetSwingTimers()
end
end
end)
UpdateWeaponSpeeds()
end)
+9
View File
@@ -927,6 +927,15 @@ pfUI:RegisterModule("thirdparty-vanilla", "vanilla", function()
end)
end)
-- SuperCleveRoidMacros
HookAddonOrVariable("SuperCleveRoidMacros", function()
if C.thirdparty.supercleveroidmacros.enable == "0" then return end
if not pfUI.bars then return end
-- disable pfUI macro scanning
pfUI.bars.skip_macro = true
end)
HookAddonOrVariable("AtlasLoot", function()
if C.thirdparty.atlasloot.enable == "0" then return end
+5 -1
View File
@@ -1,4 +1,4 @@
pfUI:RegisterModule("tooltip", "vanilla:tbc", function ()
pfUI:RegisterModule("tooltip", "vanilla", function ()
local rawborder, default_border = GetBorderSize()
pfUI.tooltip = CreateFrame('Frame', "pfTooltip", GameTooltip)
@@ -33,6 +33,10 @@ pfUI:RegisterModule("tooltip", "vanilla:tbc", function ()
tooltip.cursor:SetWidth(tonumber(C.tooltip.cursoroffset) * 2)
tooltip.cursor:SetHeight(tonumber(C.tooltip.cursoroffset) * 2)
tooltip.cursor:SetScript("OnUpdate", function()
-- throttle - cursor following doesn't need to be every frame
if (this.tick or 0) > GetTime() then return end
this.tick = GetTime() + (pfUI.throttle and pfUI.throttle:Get("tooltip_cursor") or 0.1)
local scale = UIParent:GetScale()
local x, y = GetCursorPosition()
this:SetPoint("CENTER", UIParent, "BOTTOMLEFT", x/scale, y/scale)
+2 -1
View File
@@ -27,11 +27,12 @@ pfUI:RegisterModule("totems", "vanilla:tbc", function ()
totems.OnEnter = function(self)
if not this.id then return end
local active, name, start, duration, icon = GetTotemInfo(this.id)
if not name or not active then return end -- Prüfen ob name gültig ist
local color = slots[this.id]
GameTooltip:SetOwner(this, "ANCHOR_LEFT")
GameTooltip:SetText(name, color.r+.2, color.g+.2, color.b+.2)
GameTooltip:Show()
end
end
totems.OnLeave = function(self)
GameTooltip:Hide()
+15 -5
View File
@@ -32,20 +32,30 @@ pfUI:RegisterModule("turtle-wow", "vanilla", function ()
end
end
-- refresh rip duration on ferocious bite
-- refresh rip and rake duration on ferocious bite (Turtle WoW feature)
-- Only refresh if Ferocious Bite actually hit (not missed/dodged/parried/etc.)
local match = string.find(arg1, "Ferocious Bite")
if match and arg2 then
if match and arg2 and not libdebuff:DidSpellFail("Ferocious Bite") then
local name = UnitName("target")
local level = UnitLevel("target")
-- Refresh Rip mit existierender Duration
if libdebuff.objects[name] and libdebuff.objects[name][level] and libdebuff.objects[name][level]["Rip"] then
libdebuff:AddEffect(name, level, "Rip")
local existingDuration = libdebuff.objects[name][level]["Rip"].duration
libdebuff:AddEffect(name, level, "Rip", existingDuration)
end
-- Refresh Rake mit existierender Duration
if libdebuff.objects[name] and libdebuff.objects[name][level] and libdebuff.objects[name][level]["Rake"] then
local existingDuration = libdebuff.objects[name][level]["Rake"].duration
libdebuff:AddEffect(name, level, "Rake", existingDuration)
end
end
-- refresh Immolate duration after cast Conflagrate
-- Only refresh if Conflagrate actually hit
local conflagrate = string.find(string.sub(arg1,6,17), "Conflagrate")
--arg2 is spell dmg when it hits, nil when it misses
if conflagrate and arg2 then
if conflagrate and arg2 and not libdebuff:DidSpellFail("Conflagrate") then
local name = UnitName("target")
local level = UnitLevel("target")
if libdebuff.objects[name] and libdebuff.objects[name][level] and libdebuff.objects[name][level]["Immolate"] then
+259
View File
@@ -0,0 +1,259 @@
-- UnitXP_SP3 integration module
-- Provides Line of Sight indicator, OS notifications, and enhanced targeting
-- Requires UnitXP_SP3 DLL: https://github.com/allfoxwy/UnitXP_SP3
pfUI:RegisterModule("unitxp", "vanilla", function ()
-- Check if UnitXP is available
local hasUnitXP = pcall(UnitXP, "nop", "nop")
if not hasUnitXP then return end
local rawborder, border = GetBorderSize()
-- Helper to create indicators after target frame exists
local function CreateTargetIndicators()
if not pfUI.uf or not pfUI.uf.target then return false end
-- Behind Indicator for all units (TOP)
if C.unitframes.behind_indicator == "1" and not pfUI.uf.target.behindIndicator then
local behindFrame = CreateFrame("Frame", "pfBehindIndicator", pfUI.uf.target)
behindFrame:SetAllPoints(pfUI.uf.target)
behindFrame:SetFrameLevel(pfUI.uf.target:GetFrameLevel() + 10)
behindFrame.text = behindFrame:CreateFontString(nil, "OVERLAY")
behindFrame.text:SetFont(pfUI.font_default, 13, "OUTLINE")
behindFrame.text:SetPoint("RIGHT", behindFrame, "RIGHT", -1, 7)
behindFrame.text:SetTextColor(0.3, 1, 0.3, 1)
behindFrame.text:SetText("BEHIND")
behindFrame.text:Hide()
local lastCheck = 0
behindFrame:SetScript("OnUpdate", function()
if GetTime() - lastCheck < 0.1 then return end
lastCheck = GetTime()
if not UnitExists("target") then
this.text:Hide()
return
end
local success, behind = pcall(UnitXP, "behind", "player", "target")
if success and behind then
this.text:Show()
else
this.text:Hide()
end
end)
pfUI.uf.target.behindIndicator = behindFrame
end
-- Line of Sight Indicator on Target Frame (BELOW BEHIND)
if C.unitframes.los_indicator == "1" and not pfUI.uf.target.losIndicator then
local losFrame = CreateFrame("Frame", "pfLoSIndicator", pfUI.uf.target)
losFrame:SetAllPoints(pfUI.uf.target)
losFrame:SetFrameLevel(pfUI.uf.target:GetFrameLevel() + 10)
losFrame.text = losFrame:CreateFontString(nil, "OVERLAY")
losFrame.text:SetFont(pfUI.font_default, 13, "OUTLINE")
losFrame.text:SetPoint("RIGHT", losFrame, "RIGHT", -1, -7)
losFrame.text:SetTextColor(1, 0.3, 0.3, 1)
losFrame.text:SetText("NO LOS")
losFrame.text:Hide()
local lastCheck = 0
losFrame:SetScript("OnUpdate", function()
if GetTime() - lastCheck < 0.2 then return end
lastCheck = GetTime()
if not UnitExists("target") then
this.text:Hide()
return
end
local success, inSight = pcall(UnitXP, "inSight", "player", "target")
if success and inSight == false then
this.text:Show()
else
this.text:Hide()
end
end)
pfUI.uf.target.losIndicator = losFrame
end
return true
end
-- Try to create indicators now
CreateTargetIndicators()
-- Also try on PLAYER_ENTERING_WORLD in case target frame wasn't ready
local initFrame = CreateFrame("Frame")
initFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
initFrame:RegisterEvent("PLAYER_LOGOUT")
initFrame:SetScript("OnEvent", function()
-- Handle shutdown to prevent crash 132
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
-- Stop indicator OnUpdate scripts
if pfUI.uf and pfUI.uf.target then
if pfUI.uf.target.behindIndicator then
pfUI.uf.target.behindIndicator:SetScript("OnUpdate", nil)
end
if pfUI.uf.target.losIndicator then
pfUI.uf.target.losIndicator:SetScript("OnUpdate", nil)
end
end
return
end
CreateTargetIndicators()
this:UnregisterAllEvents()
end)
-- OS Notification Support
if C.unitframes.unitxp_notify == "1" then
local notifyFrame = CreateFrame("Frame")
notifyFrame:RegisterEvent("CHAT_MSG_WHISPER")
notifyFrame:RegisterEvent("CHAT_MSG_BN_WHISPER")
notifyFrame:RegisterEvent("READY_CHECK")
notifyFrame:RegisterEvent("RAID_INSTANCE_WELCOME")
notifyFrame:RegisterEvent("PLAYER_LOGOUT")
notifyFrame:SetScript("OnEvent", function()
-- Handle shutdown to prevent crash 132
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
return
end
pcall(UnitXP, "notify", "taskbarIcon")
pcall(UnitXP, "notify", "systemSound")
end)
-- Also notify on BG queue pop
local origBattlefieldPortShow = BattlefieldFrame_Show
if origBattlefieldPortShow then
BattlefieldFrame_Show = function()
pcall(UnitXP, "notify", "taskbarIcon")
pcall(UnitXP, "notify", "systemSound")
return origBattlefieldPortShow()
end
end
end
-- Enhanced Distance API
pfUI.api.GetPreciseDistance = function(unit1, unit2)
if not unit2 then
unit2 = unit1
unit1 = "player"
end
local success, distance = pcall(UnitXP, "distanceBetween", unit1, unit2)
if success then return distance end
return nil
end
pfUI.api.IsInMeleeRange = function(unit)
local success, distance = pcall(UnitXP, "distanceBetween", "player", unit, "meleeAutoAttack")
if success and distance then
return distance <= 5
end
return nil
end
pfUI.api.GetAoEDistance = function(unit1, unit2)
if not unit2 then
unit2 = unit1
unit1 = "player"
end
local success, distance = pcall(UnitXP, "distanceBetween", unit1, unit2, "AoE")
if success then return distance end
return nil
end
-- Smart Targeting Helpers
pfUI.api.TargetNearestEnemy = function()
local success, found = pcall(UnitXP, "target", "nearestEnemy")
return success and found
end
pfUI.api.TargetHighestHP = function()
local success, found = pcall(UnitXP, "target", "mostHP")
return success and found
end
pfUI.api.TargetNextEnemy = function()
local success, found = pcall(UnitXP, "target", "nextEnemyInCycle")
return success and found
end
pfUI.api.TargetPreviousEnemy = function()
local success, found = pcall(UnitXP, "target", "previousEnemyInCycle")
return success and found
end
pfUI.api.TargetNextMarked = function(order)
local success, found = pcall(UnitXP, "target", "nextMarkedEnemyInCycle", order)
return success and found
end
pfUI.api.UnitInLineOfSight = function(unit1, unit2)
if not unit2 then
unit2 = unit1
unit1 = "player"
end
local success, inSight = pcall(UnitXP, "inSight", unit1, unit2)
if success then return inSight end
return nil
end
pfUI.api.UnitIsBehind = function(unit1, unit2)
if not unit2 then
unit2 = unit1
unit1 = "player"
end
local success, behind = pcall(UnitXP, "behind", unit1, unit2)
if success then return behind end
return nil
end
-- Debug command to test UnitXP indicators
SLASH_PFUNITXP1 = "/pfunitxp"
SlashCmdList["PFUNITXP"] = function()
local chat = DEFAULT_CHAT_FRAME
chat:AddMessage("|cff33ffccpfUI|r: UnitXP Indicator Debug")
-- Check if target exists
if not UnitExists("target") then
chat:AddMessage(" |cffff0000No target selected|r")
return
end
-- Test behind
local successB, behind = pcall(UnitXP, "behind", "player", "target")
chat:AddMessage(" Behind check: success=" .. tostring(successB) .. " value=" .. tostring(behind) .. " type=" .. type(behind))
-- Test LOS
local successL, inSight = pcall(UnitXP, "inSight", "player", "target")
chat:AddMessage(" LOS check: success=" .. tostring(successL) .. " value=" .. tostring(inSight) .. " type=" .. type(inSight))
-- Check if indicator frames exist
if pfUI.uf and pfUI.uf.target then
chat:AddMessage(" Target frame: |cff00ff00exists|r")
if pfUI.uf.target.behindIndicator then
chat:AddMessage(" Behind indicator: |cff00ff00created|r, visible=" .. tostring(pfUI.uf.target.behindIndicator:IsVisible()))
else
chat:AddMessage(" Behind indicator: |cffff0000NOT created|r (check settings)")
end
if pfUI.uf.target.losIndicator then
chat:AddMessage(" LOS indicator: |cff00ff00created|r")
else
chat:AddMessage(" LOS indicator: |cffff0000NOT created|r (check settings)")
end
else
chat:AddMessage(" Target frame: |cffff0000NOT found|r")
end
end
end)
+3 -3
View File
@@ -1,10 +1,10 @@
## Interface: 20400
## Title: |cff33ffccpf|cffffffffUI
## Author: Shagu
## Author: Shagu - modfied by me0wg4ming
## Notes: A complete user interface replacement.
## Notes-ruRU: Полная замена пользовательского интерфейса.
## Version: 5.5.4
## SavedVariables: pfUI_profiles, pfUI_addon_profiles, pfUI_cache
## Version: 7.6.2 (experiment version)
## SavedVariables: pfUI_profiles, pfUI_addon_profiles, pfUI_cache, pfUI_throttle
## SavedVariablesPerCharacter: pfUI_config, pfUI_init, pfUI_playerDB
pfUI.lua
+7
View File
@@ -30,6 +30,7 @@ pfUI_init = {}
pfUI_profiles = {}
pfUI_addon_profiles = {}
pfUI_cache = {}
pfUI_throttle = {}
-- localization
pfUI_locale = {}
@@ -47,6 +48,11 @@ pfUI.version = {}
pfUI.hooks = {}
pfUI.env = {}
-- check if macro addons are loaded (disables macrotweak/macroscan)
function pfUI:MacroAddonsLoaded()
return IsAddOnLoaded("Supermacro") or IsAddOnLoaded("SuperCleveRoidMacros") or IsAddOnLoaded("UltimaMacros")
end
-- detect current addon path
local tocs = { "", "-master", "-tbc", "-wotlk" }
for _, name in pairs(tocs) do
@@ -236,6 +242,7 @@ function pfUI:GetEnvironment()
pfUI.env._G = getfenv(0)
pfUI.env.C = pfUI_config
pfUI.env.pfUI_throttle = _G.pfUI_throttle
pfUI.env.L = (pfUI_locale[GetLocale()] or pfUI_locale["enUS"])
return pfUI.env
+3 -3
View File
@@ -1,10 +1,10 @@
## Interface: 11200
## Title: |cff33ffccpf|cffffffffUI
## Author: Shagu
## Author: Shagu - modfied by me0wg4ming
## Notes: A complete user interface replacement.
## Notes-ruRU: Полная замена пользовательского интерфейса.
## Version: 5.5.4
## SavedVariables: pfUI_profiles, pfUI_addon_profiles, pfUI_cache
## Version: 7.6.2 (experiment version)
## SavedVariables: pfUI_profiles, pfUI_addon_profiles, pfUI_cache, pfUI_throttle
## SavedVariablesPerCharacter: pfUI_config, pfUI_init, pfUI_playerDB
pfUI.lua