mirror of
https://github.com/brues-code/pfUI.git
synced 2026-09-30 03:06:01 +00:00
enhanced merge into master
Final release!
This commit is contained in:
@@ -12,6 +12,946 @@ This version includes significant performance improvements, DLL-enhanced feature
|
||||
|
||||
> **Looking for TBC support?** Visit the original pfUI by Shagu: [https://github.com/shagu/pfUI](https://github.com/shagu/pfUI)
|
||||
|
||||
## 🎯 What's New in Version 7.6.0 (February 3, 2026)
|
||||
|
||||
### 🚀 Centralized Cast-Bar Tracking System (libdebuff.lua + nameplates.lua)
|
||||
|
||||
**Major architectural change: Cast tracking moved from nameplates to libdebuff for single source of truth!**
|
||||
|
||||
Previously, both `nameplates.lua` and `libdebuff.lua` independently tracked cast events, creating code duplication and maintenance overhead. Now all cast tracking is centralized in `libdebuff.lua` with nameplates consuming shared data.
|
||||
|
||||
**libdebuff.lua - NEW Cast Tracking:**
|
||||
- ✅ `SPELL_START_SELF/OTHER` → Cast-Start Tracking
|
||||
- ✅ `SPELL_GO_SELF/OTHER` → Cast-Completion Tracking
|
||||
- ✅ `SPELL_FAILED_OTHER` → Cast-Cancel Detection (movement, interrupts, OOM)
|
||||
- ✅ `pfUI.libdebuff_casts` → Shared cast data structure `[casterGuid] = {spellID, spellName, icon, startTime, duration, endTime, event}`
|
||||
- ✅ `pfUI.libdebuff_GetSpellIcon()` → Icon cache export function
|
||||
|
||||
**nameplates.lua - Simplified Cast Consumption:**
|
||||
- ✅ `GetCastInfo(guid)` → Reads `pfUI.libdebuff_casts`
|
||||
- ✅ `pfUI.libdebuff_GetSpellIcon` → Uses shared icon cache
|
||||
- ❌ `UNIT_CASTEVENT` → **REMOVED** (replaced by Nampower SPELL_* events)
|
||||
- ❌ Local cast tracking code → **REMOVED** (~56 lines saved)
|
||||
|
||||
**Benefits:**
|
||||
- **100% Nampower, 0% SuperWOW** - No longer depends on UNIT_CASTEVENT
|
||||
- **Single Source of Truth** - Cast data only tracked once
|
||||
- **Icon Cache 100-400x faster** - First lookup via Nampower's GetSpellIconTexture, then cached
|
||||
- **Easier Maintenance** - Changes only in one place
|
||||
- **Code Reduction** - 56 lines removed from nameplates.lua
|
||||
|
||||
### ⚠️ Nampower Version Requirement Update (libdebuff.lua)
|
||||
|
||||
**Now requires Nampower 2.27.2+ (SPELL_FAILED_OTHER bug fix):**
|
||||
|
||||
Version 2.27.1 had a bug where `SPELL_FAILED_OTHER` didn't fire for movement-cancelled casts. This is now fixed in 2.27.2.
|
||||
|
||||
**User Warnings:**
|
||||
- **2.27.2+**: ✅ Success message + auto-enable CVars
|
||||
- **2.27.1**: ⚠️ Yellow warning + popup (cast-bar cancel broken)
|
||||
- **< 2.27.1**: ❌ Red error + popup (debuff tracking disabled)
|
||||
- **No Nampower**: ❌ Red error + popup (addon disabled)
|
||||
|
||||
**NEW StaticPopup Dialogs:**
|
||||
|
||||
Popups appear center-screen on login to ensure users don't miss the version requirement!
|
||||
|
||||
### 🌿 libpredict HoT Tracking Integration (libpredict.lua)
|
||||
|
||||
**Major enhancement: libdebuff integration for server-accurate HoT tracking!**
|
||||
|
||||
Previously, libpredict relied purely on prediction (UNIT_CASTEVENT + timing calculations). Now it uses libdebuff's AURA_CAST events for server-side accurate buff/debuff data when available.
|
||||
|
||||
**NEW Hybrid System:**
|
||||
```
|
||||
GetHotDuration(unit, spell):
|
||||
1. Try libdebuff first (Nampower AURA_CAST events)
|
||||
↓
|
||||
if available: return server-accurate data
|
||||
|
||||
2. Fallback to prediction (legacy system)
|
||||
↓
|
||||
Use hots[] table with UNIT_CASTEVENT prediction
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- ✅ **Server-accurate durations** - No prediction needed with Nampower
|
||||
- ✅ **Automatic rank protection** - Built into libdebuff's system
|
||||
- ✅ **Multi-caster support** - Multiple druids = multiple rejuvs tracked separately
|
||||
- ✅ **Zero overhead** - libdebuff already tracks all auras
|
||||
- ✅ **Backwards compatible** - Falls back to prediction without Nampower
|
||||
|
||||
**NEW Rank Support for HoTs:**
|
||||
|
||||
Extended `Hot()` function signature to include rank parameter:
|
||||
```lua
|
||||
function libpredict:Hot(sender, target, spell, duration, startTime, source, rank)
|
||||
```
|
||||
|
||||
**Rank Protection Logic:**
|
||||
- Don't overwrite Rank 10 HoT with Rank 8!
|
||||
- Active higher-rank HoTs block lower-rank applications
|
||||
- Works with multiple casters simultaneously
|
||||
|
||||
**HealComm Protocol Extended (Backwards Compatible):**
|
||||
- OLD: `"Reju/TargetName/15/"`
|
||||
- NEW: `"Reju/TargetName/15/10/"` (rank added)
|
||||
- `"0"` = unknown rank (for non-rank-aware clients)
|
||||
|
||||
**Example Scenario:**
|
||||
```
|
||||
Druid A casts Rejuvenation Rank 10 (15s duration)
|
||||
Druid B casts Rejuvenation Rank 8 (12s duration)
|
||||
|
||||
With rank protection:
|
||||
→ Rank 8 is BLOCKED while Rank 10 is active!
|
||||
→ No more accidental overwrites of better HoTs!
|
||||
```
|
||||
|
||||
### 📊 Code Statistics
|
||||
|
||||
**libdebuff.lua:**
|
||||
- Lines: 2743 → 2835 (+92 lines)
|
||||
- Events: 12 → 15 (+3: SPELL_START_SELF/OTHER, SPELL_FAILED_OTHER)
|
||||
- Exports: 14 → 16 (+2: pfUI.libdebuff_casts, pfUI.libdebuff_GetSpellIcon)
|
||||
|
||||
**nameplates.lua:**
|
||||
- Lines: 1826 → 1770 (-56 lines)
|
||||
- Events: 7 → 6 (-1: UNIT_CASTEVENT removed)
|
||||
- Code removed: ~74 lines (UNIT_CASTEVENT handler, local cast tracking)
|
||||
|
||||
**libpredict.lua:**
|
||||
- Lines: 935 → 1095 (+160 lines)
|
||||
- New: libdebuff integration, rank support, rank protection logic
|
||||
- Backwards compatible: Works with/without Nampower
|
||||
|
||||
---
|
||||
|
||||
## 🎯 What's New in Version 7.5.1 (February 02, 2026)
|
||||
|
||||
- Added icon cache system - Icons are now cached in pfUI.libdebuff_icon_cache for instant lookups after first access
|
||||
|
||||
- Replaced SpellInfo texture lookups with GetSpellIconTexture - Direct DBC queries via Nampower (~100-400x faster than tooltip parsing)
|
||||
|
||||
- Optimized UnitDebuff() function - Now uses GetUnitField("aura") to retrieve spell IDs directly from unit data, then fetches icons via GetSpellIconTexture instead of vanilla UnitDebuff API
|
||||
|
||||
- Changed fallback icons - Unknown spell icons now display QuestionMark instead of class-specific icons
|
||||
|
||||
- Performance impact - Icon lookups reduced from ~5-20ms to ~0.05ms (first) / ~0.001ms (cached) per debuff, resulting in 600-2600x speedup for full debuff bars
|
||||
|
||||
- Replaced in libdebuff.lua the UNIT_CASTEVENT of Superwow with Nampowers SPELL_GO and SPELL_START events (slowly trying to get away from superwow, not maintained anymore and outdated)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 What's New in Version 7.5.0 (January 31, 2026)
|
||||
|
||||
### 🔧 Player Buff Bar Timer Fix (buffwatch.lua)
|
||||
|
||||
**Fixed buff timers resetting on Player Buff/Debuff Bars when other buffs expire:**
|
||||
|
||||
Previously, buff bar timers would reset or jump when other buffs expired because the UUID (unique identifier) included the slot number. Since slots shift when buffs expire, the same buff would get a new UUID, causing the timer bar to think it's a new buff.
|
||||
|
||||
**The Problem:**
|
||||
- Old UUID: `texture + name + slot` (e.g., "PowerWordFortitude_tex_PWF_3")
|
||||
- Buff in slot 3 expires → slots 4,5,6 shift down to 3,4,5
|
||||
- UUID changes from `..._4` to `..._3` → timer resets!
|
||||
|
||||
**The Solution:**
|
||||
- Player buffs now use: `texture + name` only (no slot)
|
||||
- Target debuffs still use: `texture + name + slot` (needed for multi-caster scenarios)
|
||||
|
||||
```lua
|
||||
-- For player: no slot in uuid (slots shift when other buffs expire)
|
||||
-- For target: include slot (multiple players can have same debuff)
|
||||
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
|
||||
```
|
||||
|
||||
### 🛡️ Immunity Check for Debuff Timers (libdebuff.lua)
|
||||
|
||||
**No more phantom timers for immune targets:**
|
||||
|
||||
When a target is immune to your debuff (e.g., Rake bleed on a bleed-immune mob), the `AURA_CAST` event fires but `DEBUFF_ADDED` never comes. Previously this could create a timer with icon for a debuff that was never actually applied.
|
||||
|
||||
**The Fix:**
|
||||
- Debuff data now requires `slot` to be set (confirmed by `DEBUFF_ADDED_OTHER` event)
|
||||
- If `AURA_CAST` fires but `DEBUFF_ADDED` never comes → `slot` stays `nil` → no timer/icon shown
|
||||
|
||||
```lua
|
||||
-- IMMUNITY CHECK: Only show if slot is set (confirmed by DEBUFF_ADDED_OTHER)
|
||||
-- This prevents showing timers for spells like Rake where the bleed is immune
|
||||
if data.slot and timeleft > -1 then
|
||||
-- Show the debuff
|
||||
end
|
||||
```
|
||||
|
||||
### 🎯 UnitDebuff() Now Returns Caster Information (libdebuff.lua)
|
||||
|
||||
**Enhanced UnitDebuff() API - 8th return value is now `caster`:**
|
||||
|
||||
```lua
|
||||
local name, rank, texture, stacks, dtype, duration, timeleft, caster = libdebuff:UnitDebuff(unit, id)
|
||||
-- caster = "player" (your debuff), "other" (someone else's), or nil (unknown)
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- Buff bar tooltip can now find correct slot for "only own debuffs" mode
|
||||
- UI can differentiate between your debuffs and others' debuffs
|
||||
- Enables future features like "show only my debuffs" filters
|
||||
|
||||
### 🔄 Buff Bar Tooltip Fix for "Only Own Debuffs" Mode (buffwatch.lua)
|
||||
|
||||
**Fixed tooltip showing wrong debuff in "only own debuffs" mode:**
|
||||
|
||||
When using the "Show only own debuffs" option on Target Debuff Bars, hovering over a debuff could show the wrong tooltip because the displayed slot didn't match the actual game slot. Now searches through all game slots to find the correct one by matching spell name AND caster.
|
||||
|
||||
### 🔧 Lua 5.0 Local Variable Limit Workaround (libdebuff.lua)
|
||||
|
||||
**Fixed addon failing to load due to Lua 5.0's 32 local variable limit:**
|
||||
|
||||
Lua 5.0 (used by WoW 1.12) has a hard limit of 32 local variables per function scope. As libdebuff grew, it hit this limit and stopped loading entirely.
|
||||
|
||||
**The Solution:** Moved 11 tables from local scope to `pfUI.` namespace:
|
||||
|
||||
| Old (local) | New (pfUI. namespace) |
|
||||
|-------------|----------------------|
|
||||
| `ownDebuffs` | `pfUI.libdebuff_own` |
|
||||
| `ownSlots` | `pfUI.libdebuff_own_slots` |
|
||||
| `allSlots` | `pfUI.libdebuff_all_slots` |
|
||||
| `allAuraCasts` | `pfUI.libdebuff_all_auras` |
|
||||
| `pendingCasts` | `pfUI.libdebuff_pending` |
|
||||
| `objectsByGuid` | `pfUI.libdebuff_objects_guid` |
|
||||
| `debugStats` | `pfUI.libdebuff_debugstats` |
|
||||
| `lastCastRanks` | `pfUI.libdebuff_lastranks` |
|
||||
| `lastFailedSpells` | `pfUI.libdebuff_lastfailed` |
|
||||
| `lastUnitDebuffLog` | `pfUI.libdebuff_lastlog` |
|
||||
| `cache` | `pfUI.libdebuff_cache` |
|
||||
|
||||
### 🛑 Crash 132 Fix (Credits: jrc13245)
|
||||
|
||||
**Fixed client crash (Error 132) when logging out:**
|
||||
|
||||
WoW crashes with Error 132 when addons make API calls like `UnitExists()` during shutdown, especially with UnitXP DLL installed.
|
||||
|
||||
**The Fix:** Register `PLAYER_LOGOUT` event and immediately disable all event handling:
|
||||
|
||||
```lua
|
||||
frame:RegisterEvent("PLAYER_LOGOUT")
|
||||
frame:SetScript("OnEvent", function()
|
||||
if event == "PLAYER_LOGOUT" then
|
||||
this:UnregisterAllEvents()
|
||||
this:SetScript("OnEvent", nil)
|
||||
return
|
||||
end
|
||||
-- ... rest of event handling
|
||||
end)
|
||||
```
|
||||
|
||||
**Applied to:**
|
||||
- `libdebuff.lua` - All event frames
|
||||
- `nameplates.lua` - nameplates + nameplates.combat frames
|
||||
- `nampower.lua` - Spell queue indicator frame
|
||||
- `superwow.lua` - Secondary mana bar frames
|
||||
- `actionbar.lua` - Page switch frame
|
||||
|
||||
### ⚡ Performance Micro-Optimizations
|
||||
|
||||
**Various small performance improvements across the codebase:**
|
||||
|
||||
| Optimization | Location | Benefit |
|
||||
|-------------|----------|---------|
|
||||
| `childs` table reuse | nameplates.lua | Avoids creating new table every scan cycle |
|
||||
| Indexed access instead of `pairs()` | nameplates.lua | Faster debuff timeout scanning |
|
||||
| Quick exit if not in combat | nameplates.lua | Skips threatcolor calculation when unnecessary |
|
||||
| Player GUID caching | libdebuff.lua | Avoids repeated `UnitExists("player")` calls |
|
||||
| Consistent DoNothing() pattern | unitframes.lua, nameplates.lua | Lightweight frames when animation disabled |
|
||||
|
||||
### 📋 Complete libdebuff.lua Feature Summary
|
||||
|
||||
For reference, here's everything the enhanced libdebuff system now provides:
|
||||
|
||||
**Debuff Detection:**
|
||||
- ✅ Checks for dodges, misses, resists, parries, immunes, reflects, and evades
|
||||
- ✅ Immunity check - no timer if debuff wasn't actually applied
|
||||
- ✅ Tracks if debuff is from YOU or from OTHERS (including their GUID)
|
||||
|
||||
**Rank & Duration:**
|
||||
- ✅ Rank protection - lower rank spells can't refresh higher rank timers
|
||||
- ✅ Shared debuff logic (`uniqueDebuffs` and `debuffOverwritePairs`)
|
||||
- ✅ Faerie Fire ↔ Faerie Fire (Feral), Demo Shout ↔ Demo Roar overwrites (with rank check!)
|
||||
- ✅ Combo point finisher duration (Rip, Rupture, Kidney Shot)
|
||||
- ✅ Talent-based duration modifiers (Booming Voice, Improved SW:P, etc.)
|
||||
|
||||
**Tracking:**
|
||||
- ✅ Multi-target debuff tracking via GUID
|
||||
- ✅ Debuff stack tracking for stackable debuffs
|
||||
- ✅ Handles dispels and removals via events
|
||||
|
||||
**API:**
|
||||
- ✅ `UnitDebuff()` returns: name, rank, texture, stacks, dtype, duration, timeleft, caster
|
||||
- ✅ `UnitOwnDebuff()` for filtering only your own debuffs
|
||||
- ✅ Cleveroids API compatibility via `objectsByGuid`
|
||||
|
||||
**Debug Commands:**
|
||||
- `/shifttest start/stop/stats/slots` - Debug debuff slot tracking
|
||||
- `/memcheck` - Show memory usage statistics
|
||||
|
||||
---
|
||||
|
||||
## 🎯 What's New in Version 7.4.3 (January 29, 2026)
|
||||
|
||||
### ⚡ Massive Performance Optimization - Cooldown Frame Overhaul
|
||||
|
||||
**Revolutionary frame creation system that eliminates unnecessary Model frames:**
|
||||
|
||||
Previously, pfUI created expensive Model frames for every single buff/debuff cooldown timer, even when the animation was disabled. This caused significant performance overhead, especially in 40-man raids where hundreds of frames were created but never actually used.
|
||||
|
||||
**The Problem:**
|
||||
- Old system: **ALWAYS** created Model frames with `CooldownFrameTemplate`
|
||||
- When animation was disabled, frames were just hidden with `SetAlpha(0)`
|
||||
- The frames still existed and consumed CPU resources in the background
|
||||
- In raids: 40 players × 32 buffs/debuffs = **1,280 Model frames** running even when animations were off!
|
||||
|
||||
**The Solution (Nameplates.lua + Unitframes.lua):**
|
||||
- New system: Creates **Frame type based on config setting**
|
||||
- Animation ON → Model frame with `CooldownFrameTemplate` (expensive but animated)
|
||||
- Animation OFF → Regular Frame with dummy functions (lightweight, no animation)
|
||||
- Dummy functions (`DoNothing()`) prevent crashes when `CooldownFrame_SetTimer()` is called
|
||||
|
||||
**Performance Impact:**
|
||||
|
||||
| Scenario | Before 7.4.3 | After 7.4.3 | Improvement |
|
||||
|----------|--------------|-------------|-------------|
|
||||
| Player frame (animation ON) | 32 Model frames | 32 Model frames | No change ✅ |
|
||||
| 40 raid frames (animation OFF) | 1,280 Model frames | 1,280 Light frames | **100% lighter!** 🚀 |
|
||||
| Mixed (player ON, raid OFF) | 1,312 Model frames | 32 Model + 1,280 Light | **98% less Model frames!** 🎯 |
|
||||
|
||||
**Real-World Example:**
|
||||
```
|
||||
Before: ALL frames = 1,312 expensive Model frames
|
||||
After: 32 Model (player) + 1,280 Light (raid) = 98% reduction in overhead
|
||||
```
|
||||
|
||||
**Technical Implementation:**
|
||||
|
||||
```lua
|
||||
-- Nameplates.lua & Unitframes.lua
|
||||
if cooldown_anim == 1 then
|
||||
-- Create expensive Model frame
|
||||
cd = CreateFrame("Model", ...)
|
||||
else
|
||||
-- Create lightweight Frame
|
||||
cd = CreateFrame("Frame", ...)
|
||||
cd.AdvanceTime = DoNothing
|
||||
cd.SetSequence = DoNothing
|
||||
cd.SetSequenceTime = DoNothing
|
||||
end
|
||||
```
|
||||
|
||||
**User Experience:**
|
||||
- ✅ **GUI Integration:** Toggling "Show Timer Animation" now prompts for `/reload`
|
||||
- ✅ **Per-Frame Control:** Each unitframe type (player/target/raid/party) has independent settings
|
||||
- ✅ **Immediate Effect:** Reload applies the correct frame type based on your config
|
||||
- ✅ **No Visual Change:** When animation is ON, everything looks identical (just way more efficient!)
|
||||
|
||||
**Why This Matters:**
|
||||
- **40-man raids:** Dramatically reduced frame update overhead
|
||||
- **Low-end PCs:** Smoother gameplay with animations disabled
|
||||
- **Battery life:** Less CPU usage = longer laptop battery
|
||||
- **Future-proof:** Foundation for more performance optimizations
|
||||
|
||||
**Compatibility:**
|
||||
- Works with existing timer text display (independent of animation)
|
||||
- Fully backward compatible with all existing configs
|
||||
- No changes needed to user settings (automatic on reload)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 What's New in Version 7.4.2 (January 28, 2026)
|
||||
|
||||
### Major Performance Improvements
|
||||
- **Much faster debuff tracking** - No more lag in 40-man raids
|
||||
- **10x less memory usage** - Runs cleaner over long raid sessions
|
||||
- **Instant cleanup** - Dead mobs cleaned up immediately (was: 2-5 minutes)
|
||||
|
||||
### Better Debuff Tracking
|
||||
- **Multi-player debuffs** - See debuffs from all raid members with accurate timers
|
||||
- **Rank protection** - Lower rank spells can't overwrite higher ranks anymore
|
||||
- **100% accurate positioning** - Debuff icons always in the correct slot
|
||||
- **Better combo points** - Rip, Rupture, Kidney Shot show correct duration
|
||||
|
||||
---
|
||||
|
||||
## 🎯 What's New in Version 7.4.1 (January 27, 2026)
|
||||
|
||||
### 🎯 Nameplate Debuff Timer Improvements
|
||||
|
||||
- ✅ **New Option: Enable Debuff Timers** - Toggle for debuff timer display on nameplates
|
||||
- Moved from hidden location (Appearance → Cooldown → "Display Debuff Durations") to Nameplates → Debuffs
|
||||
- All timer-related options are now grouped together for better discoverability
|
||||
- ✅ **New Option: Show Timer Text** - Toggle the countdown text (e.g., "12s") on debuff icons
|
||||
- Previously always shown, now configurable
|
||||
- ✅ **Show Timer Animation** - Existing pie-chart animation option, now properly grouped with other timer options
|
||||
|
||||
### 🖼️ Unitframe Timer Config Fix (unitframes.lua)
|
||||
|
||||
- ✅ **Live Config Updates** - "Show Timer Animation" and "Show Timer Text" now update immediately
|
||||
- Previously: Changes only applied after buffs/debuffs were refreshed
|
||||
- Now: Toggling the option instantly shows/hides the animation and text on existing buffs/debuffs
|
||||
|
||||
### 🔧 Slot Shifting Fix Attempt (libdebuff.lua)
|
||||
|
||||
- ✅ **DEBUFF_REMOVED now uses slotData.spellName** - Previously used spellName from scan, which could be wrong after slot shifting
|
||||
- When debuffs shift slots (e.g., slot 3 removed, slots 4+ shift down), the scan might read a different spell
|
||||
- Now uses `removedSpellName = slotData.spellName` from stored slot data for consistency
|
||||
- ✅ **Cleanup empty spell tables** - After removing a caster from allAuraCasts, checks if no other casters remain and removes the empty spell table
|
||||
- ✅ **Defensive casterGuid validation** - Checks for empty string and "0x0000000000000000" before looking up timer data
|
||||
- ✅ **Invalid timer detection** - Warns when remaining > duration (impossible state)
|
||||
- ✅ **ValidateSlotConsistency function** - Debug function to verify allSlots and allAuraCasts consistency after shifting
|
||||
- ✅ **Enhanced debug logging** - All debug messages now include target= for easier filtering
|
||||
|
||||
---
|
||||
|
||||
## 🎯 What's New in Version 7.4.0 (January 26, 2026)
|
||||
|
||||
### 🗡️ Rogue Combo Point Fix
|
||||
|
||||
**PLAYER_COMBO_POINTS event now works for Rogues:**
|
||||
|
||||
The combo point tracking was previously only enabled for Druids. Rogues were completely ignored, causing abilities like Kidney Shot to always show base duration (1 sec) instead of the correct CP-scaled duration.
|
||||
|
||||
**Technical Details:**
|
||||
- Nampower sends `durationMs=1000` (base duration) for Kidney Shot
|
||||
- Code checked `if duration == 0` before calling `GetDuration()`
|
||||
- Since duration was 1 (not 0), the CP calculation was skipped
|
||||
- Fix: Always call `GetDuration()` for CP-based abilities, regardless of event duration
|
||||
|
||||
### ⚙️ New Settings: Number & Timer Formatting
|
||||
|
||||
**Abbreviate Numbers (Settings → General):**
|
||||
|
||||
| Option | Example |
|
||||
|--------|---------|
|
||||
| Full Numbers | 4250 |
|
||||
| 2 Decimals | 4.25k |
|
||||
| 1 Decimal | 4.2k (always rounds DOWN) |
|
||||
|
||||
**Castbar Timer Decimals (Settings → General):**
|
||||
|
||||
| Option | Example |
|
||||
|--------|---------|
|
||||
| 1 Decimal | 2.1 |
|
||||
| 2 Decimals | 2.14 |
|
||||
|
||||
### 🎬 Nameplate Castbar Improvements
|
||||
|
||||
**Smooth Castbar Animation:**
|
||||
- Fixed stuttering castbar caused by incorrect throttle placement
|
||||
- Scanner throttle (0.05s) now only affects nameplate detection
|
||||
- Castbar updates run at full 50 FPS for smooth animation
|
||||
|
||||
**Countdown Timer:**
|
||||
- Castbar timer now counts DOWN (3.0 → 0.0) instead of up
|
||||
- Shows remaining cast time, not elapsed time
|
||||
|
||||
**Intelligent Throttling (unchanged):**
|
||||
- Target OR casting nameplates: 0.02s (50 FPS)
|
||||
- All other nameplates: 0.1s (10 FPS)
|
||||
- Event updates bypass throttle entirely
|
||||
|
||||
### 🧹 Memory Management
|
||||
|
||||
**Cache cleanup for hidden nameplates:**
|
||||
- `guidRegistry` cleared when plate hides
|
||||
- `CastEvents` cleared when plate hides
|
||||
- `debuffCache` cleared when plate hides
|
||||
- `threatMemory` cleared when plate hides
|
||||
|
||||
Prevents memory leaks when mobs die or go out of range.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 What's New in Version 7.3.0 (January 25, 2026)
|
||||
|
||||
### ⚡ O(1) Performance Optimizations for Unitframes
|
||||
|
||||
**Complete rewrite of health/mana lookups using Nampower's `GetUnitField` API:**
|
||||
|
||||
The unitframes now use direct memory access via `GetUnitField(guid, "health")` instead of the slower `UnitHealth()` API calls. This provides significant performance improvements especially in raids.
|
||||
|
||||
**Key Changes:**
|
||||
|
||||
| Component | Before (7.2.0) | After (7.3.0) |
|
||||
|-----------|----------------|---------------|
|
||||
| HealPredict Health | `UnitHealth()` API calls | `GetUnitField(guid, "health")` O(1) |
|
||||
| Health Bar Colors | 4x redundant API calls per update | Uses cached `hp_orig`/`hpmax_orig` values |
|
||||
| GetColor Function | `UnitHealth()` API calls | `GetUnitField(guid, "health")` O(1) |
|
||||
|
||||
**Fallback Support:**
|
||||
- Automatic fallback to `UnitHealth()` when Nampower not available
|
||||
- Automatic fallback for units >180 yards (out of Nampower range)
|
||||
- Automatic fallback when GUID unavailable
|
||||
|
||||
### 🚀 Smart Roster Updates (No More Freeze!)
|
||||
|
||||
**GUID-based tracking eliminates screen freezes when swapping raid groups:**
|
||||
|
||||
Previously, any raid roster change would trigger a full update of ALL 40 raid frames, causing noticeable freezes. Now, only frames where the actual player changed get updated.
|
||||
|
||||
**How it works:**
|
||||
```lua
|
||||
-- OLD: RAID_ROSTER_UPDATE → ALL 40 frames update_full = true → FREEZE
|
||||
-- NEW: RAID_ROSTER_UPDATE → Check GUID per frame → Only changed frames update
|
||||
```
|
||||
|
||||
| Scenario | Before (7.2.0) | After (7.3.0) |
|
||||
|----------|----------------|---------------|
|
||||
| Swap 2 players | 40 frame updates | 2 frame updates |
|
||||
| Player joins | 40 frame updates | 1 frame update |
|
||||
| Player leaves | 40 frame updates | 1 frame update |
|
||||
| No changes | 40 frame updates | 0 frame updates |
|
||||
|
||||
**Technical Implementation:**
|
||||
- `pfUI.uf.guidTracker` tracks GUID per frame
|
||||
- On roster change, compares old GUID vs new GUID
|
||||
- Only sets `update_full = true` if GUID actually changed
|
||||
- Also forces `update_aura = true` to refresh buffs/debuffs
|
||||
|
||||
### 🔧 libpredict.lua Optimizations
|
||||
|
||||
**Eliminated redundant `UnitName()` calls:**
|
||||
- `UnitGetIncomingHeals()`: Removed double `UnitName()` call
|
||||
- `UnitHasIncomingResurrection()`: Removed double `UnitName()` call
|
||||
- `UNIT_HEALTH` event handler: Reuses cached name variable
|
||||
|
||||
---
|
||||
|
||||
## 🎯 What's New in Version 7.2.0 (January 24, 2026)
|
||||
|
||||
### 🐱 Druid Secondary Mana Bar Overhaul
|
||||
|
||||
**Complete rewrite using Nampower's `GetUnitField` API:**
|
||||
|
||||
The Druid Mana Bar feature (showing base mana while in shapeshift form) has been completely rewritten to use Nampower's native `GetUnitField` instead of the deprecated `UnitMana()` extended return values.
|
||||
|
||||
**Key Changes:**
|
||||
|
||||
| Component | Before (7.1.0) | After (7.2.0) |
|
||||
|-----------|----------------|---------------|
|
||||
| Data Source | `UnitMana()` second return value | `GetUnitField(guid, "power1")` |
|
||||
| Player Support | ✅ Druids only | ✅ Druids only |
|
||||
| Target Support | ❌ Limited/broken | ✅ All classes can see Druid mana in all forms |
|
||||
| Text Settings | Hardcoded format | Respects Power Bar text config |
|
||||
|
||||
<img width="704" height="210" alt="grafik" src="https://i.ibb.co/bgfC04Gk/grafik.png" />
|
||||
|
||||
**New Features:**
|
||||
- ✅ **Target Secondary Mana:** See enemy/friendly Druid's base mana while they're in Cat/Bear form
|
||||
- ✅ **Respects Power Text Settings:** Uses same format as your Power Bar configuration (`powerdyn`, `power`, `powerperc`, `none`, etc.)
|
||||
- ✅ **Available for ALL Classes:** Any class can now see Druid mana bars (controlled by "Show Druid Mana Bar" setting)
|
||||
|
||||
**Technical Implementation:**
|
||||
```lua
|
||||
-- OLD: Extended UnitMana (unreliable for other units)
|
||||
local _, baseMana = UnitMana("target") -- Often returns nil for non-player
|
||||
|
||||
-- NEW: Direct field access via Nampower
|
||||
local _, guid = UnitExists("target")
|
||||
local baseMana = GetUnitField(guid, "power1") -- Base mana
|
||||
local baseMaxMana = GetUnitField(guid, "maxPower1") -- Max base mana
|
||||
```
|
||||
|
||||
### 🧹 Major Code Cleanup
|
||||
|
||||
**superwow.lua:**
|
||||
- ❌ Removed legacy `pfDruidMana` bar (old SuperWoW-style implementation)
|
||||
- ❌ Removed `UnitMana()` fallback code
|
||||
- ✅ Unified all secondary mana bars to use `GetUnitField`
|
||||
- ✅ Fixed text centering issue (was using `SetJustifyH("RIGHT")`)
|
||||
|
||||
**nampower.lua - Massive Cleanup:**
|
||||
|
||||
Removed significant amounts of dead/unused code:
|
||||
|
||||
| Removed Feature | Reason |
|
||||
|-----------------|--------|
|
||||
| Buff tracking system | Data collected but never displayed |
|
||||
| HoT Detection (AURA_CAST events) | `OnHotApplied` callback never implemented |
|
||||
| Swing Timer (`GetSwingTimers()`) | Never called anywhere in codebase |
|
||||
| UNIT_DIED buff/debuff cleanup | Now handled by libdebuff |
|
||||
|
||||
**Result:** Cleaner, more maintainable code with reduced memory footprint.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 What's New in Version 7.1.0 (January 24, 2026)
|
||||
|
||||
### ⚡ Cooldown Timer Animation Support
|
||||
|
||||
**Nameplate Debuff Animations:**
|
||||
- ✅ Added "Show Timer Animation" option for nameplate debuffs
|
||||
- ✅ Uses proper `Model` frame with `CooldownFrameTemplate` for Vanilla client
|
||||
- ✅ Pie/swipe animation now works on nameplate debuff icons
|
||||
- ✅ Configurable via GUI: Nameplates → Show Timer Animation
|
||||
|
||||
**Target Frame Debuff Animations:**
|
||||
- ✅ Timer animations now properly visible on target/player frame debuffs
|
||||
- ✅ Fixed CD frame scaling and positioning for correct display
|
||||
- ✅ `SetScale(size/32)`, `SetAllPoints()`, `SetFrameLevel(14)` for proper rendering
|
||||
|
||||
**cooldown.lua Fix:**
|
||||
- ✅ Added `elseif pfCooldownStyleAnimation == 1 then SetAlpha(1)` to make animations visible
|
||||
- ✅ Previously animations were created but never shown (alpha stayed 0)
|
||||
|
||||
### 🧹 Memory Leak Fixes
|
||||
|
||||
**libdebuff.lua:**
|
||||
- ✅ `lastCastRanks` table now cleaned up (entries older than 3 seconds removed)
|
||||
- ✅ `lastFailedSpells` table now cleaned up (entries older than 2 seconds removed)
|
||||
- ✅ Previously these tables grew indefinitely over long play sessions
|
||||
|
||||
**unitframes.lua:**
|
||||
- ✅ Cache cleanup now uses in-place `= nil` instead of creating new table every 30 seconds
|
||||
- ✅ Reduces garbage collector pressure
|
||||
|
||||
**nameplates.lua:**
|
||||
- ✅ Reusable `debuffSeen` table instead of creating `local seen = {}` on every DEBUFF_UPDATE event
|
||||
- ✅ Significant reduction in table allocations during combat
|
||||
|
||||
---
|
||||
|
||||
## 🎯 What's New in Version 7.0.0 (January 21, 2026)
|
||||
|
||||
### 🔥 Complete libdebuff.lua Rewrite (464 → 1594 lines)
|
||||
|
||||
**Event-Driven Architecture:**
|
||||
|
||||
Replaced tooltip scanning with a pure event-based system using Nampower/SuperWoW:
|
||||
|
||||
**OLD (Master):**
|
||||
```lua
|
||||
-- Every UI update:
|
||||
for slot = 1, 16 do
|
||||
scanner:SetUnitDebuff("target", slot) -- Tooltip scan
|
||||
local name = scanner:Line(1)
|
||||
end
|
||||
```
|
||||
|
||||
**NEW (Experiment):**
|
||||
```lua
|
||||
-- Events fire when changes happen:
|
||||
RegisterEvent("AURA_CAST_ON_SELF") -- You cast a debuff
|
||||
RegisterEvent("DEBUFF_ADDED_OTHER") -- Debuff lands in slot
|
||||
RegisterEvent("DEBUFF_REMOVED_OTHER") -- Debuff removed
|
||||
|
||||
-- UI reads from pre-computed tables:
|
||||
local data = ownDebuffs[guid][spell] -- Direct lookup
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🐱 Combo Point Finisher Support
|
||||
|
||||
**Dynamic Duration Calculation:**
|
||||
|
||||
| Ability | Formula | Durations (1-5 CP) |
|
||||
|---------|---------|-------------------|
|
||||
| Rip | 8s + CP × 2s | 10s / 12s / 14s / 16s / 18s |
|
||||
| Rupture | 10s + CP × 2s | 12s / 14s / 16s / 18s / 20s |
|
||||
| Kidney Shot | 2s + CP × 1s | 3s / 4s / 5s / 6s / 7s |
|
||||
|
||||
**Before:** All Rips showed 18s (wrong for 1-4 CP)
|
||||
**After:** Shows actual duration based on combo points used
|
||||
|
||||
---
|
||||
|
||||
### 🎭 Carnage Talent Detection
|
||||
|
||||
**Ferocious Bite Refresh Mechanics:**
|
||||
- Tracks Carnage talent (Rank 2) which makes Ferocious Bite refresh Rip & Rake
|
||||
- Only refreshes when Ferocious Bite HITS (not on miss/dodge/parry)
|
||||
- Preserves original duration (doesn't reset to new CP count)
|
||||
- Uses `DidSpellFail()` API for miss detection
|
||||
|
||||
---
|
||||
|
||||
### 🔄 Additional Features
|
||||
|
||||
- **Debuff Overwrite Pairs:** Faerie Fire ↔ Faerie Fire (Feral), Demoralizing Shout ↔ Demoralizing Roar
|
||||
- **Slot Shifting Algorithm:** Accurate icon placement when debuffs expire
|
||||
- **Multi-Caster Tracking:** Multiple players' debuffs tracked separately
|
||||
- **Rank Protection:** Lower rank can't overwrite higher rank timer
|
||||
- **Unique Debuff System:** Hunter's Mark, Scorpid Sting, etc. handled correctly
|
||||
|
||||
---
|
||||
|
||||
## 📊 Performance Comparison
|
||||
|
||||
### The Core Difference: Data Access Architecture
|
||||
|
||||
**Master uses Blizzard API + Tooltip Scanning:**
|
||||
```lua
|
||||
-- Every UnitDebuff call requires tooltip scan
|
||||
function libdebuff:UnitDebuff(unit, id)
|
||||
local texture, stacks, dtype = UnitDebuff(unit, id)
|
||||
if texture then
|
||||
scanner:SetUnitDebuff(unit, id) -- Tooltip scan to get spell name
|
||||
effect = scanner:Line(1)
|
||||
end
|
||||
-- Duration comes from hardcoded lookup tables
|
||||
end
|
||||
|
||||
-- UnitOwnDebuff iterates all 16 slots
|
||||
function libdebuff:UnitOwnDebuff(unit, id)
|
||||
for i = 1, 16 do
|
||||
local effect = libdebuff:UnitDebuff(unit, i) -- 16 tooltip scans!
|
||||
if caster == "player" then ...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Experiment uses Nampower Events + GetUnitField:**
|
||||
```lua
|
||||
-- Single call returns ALL 48 aura slots (32 buffs + 16 debuffs)
|
||||
local auras = GetUnitField(guid, "aura") -- Returns array[48] of spell IDs
|
||||
local stacks = GetUnitField(guid, "auraApplications") -- Returns array[48] of stack counts
|
||||
|
||||
-- Events fire with full data including duration
|
||||
-- AURA_CAST_ON_OTHER: spellId, casterGuid, targetGuid, effect, effectAuraName,
|
||||
-- effectAmplitude, effectMiscValue, durationMs, auraCapStatus
|
||||
-- BUFF_REMOVED_OTHER: guid, slot, spellId, stackCount, auraLevel
|
||||
|
||||
-- UnitOwnDebuff is just a table lookup
|
||||
function libdebuff:UnitOwnDebuff(unit, id)
|
||||
local _, guid = UnitExists(unit)
|
||||
local data = ownDebuffs[guid][spellName] -- Pre-computed by events
|
||||
return data.duration, data.timeleft, ...
|
||||
end
|
||||
```
|
||||
|
||||
### Nampower Features Used (Experiment Only)
|
||||
|
||||
| Feature | Purpose | Data Provided |
|
||||
|---------|---------|---------------|
|
||||
| `GetUnitField(guid, "aura")` | Single call returns all 48 aura spell IDs | `array[48]` of spell IDs |
|
||||
| `GetUnitField(guid, "auraApplications")` | Stack counts for all auras | `array[48]` of stack counts |
|
||||
| `GetUnitField(guid, "power1")` | Base mana for shapeshifted Druids | Mana value (7.2.0) |
|
||||
| `GetUnitField(guid, "maxPower1")` | Max base mana | Max mana value (7.2.0) |
|
||||
| `AURA_CAST_ON_OTHER` | Instant debuff cast detection | spellId, casterGuid, targetGuid, **durationMs** |
|
||||
| `AURA_CAST_ON_SELF` | Instant self-buff detection | Same as above |
|
||||
| `BUFF_REMOVED_OTHER` | Instant aura removal detection | guid, **slot**, spellId, stackCount |
|
||||
| `DEBUFF_ADDED_OTHER` | Debuff slot assignment | guid, slot, spellId, stacks |
|
||||
| `DEBUFF_REMOVED_OTHER` | Debuff removal with slot info | guid, slot, spellId |
|
||||
|
||||
Master uses **none** of these - it relies on:
|
||||
- `UnitDebuff()` API (no caster info, no duration)
|
||||
- Tooltip scanning via `GameTooltip:SetUnitDebuff()` to get spell names
|
||||
- Chat message parsing (`CHAT_MSG_SPELL_PERIODIC_*`) for duration detection
|
||||
- Hardcoded duration lookup tables
|
||||
|
||||
### Performance Comparison
|
||||
|
||||
| Operation | Master | Experiment | Improvement |
|
||||
|-----------|--------|------------|-------------|
|
||||
| Initial target scan | 16 tooltip scans | 1 GetUnitField call (48 slots) | **16x fewer calls** |
|
||||
| Get YOUR debuffs | Loop 16 slots + tooltip each | Direct table lookup | **~50-100x faster** |
|
||||
| Debuff duration | Hardcoded tables / chat parsing | Event provides `durationMs` | **Accurate to ms** |
|
||||
| Detect debuff removal | Polling / timeout | `BUFF_REMOVED_OTHER` event | **Instant** |
|
||||
| Detect new debuff | Chat message delay | `AURA_CAST_ON_OTHER` event | **Instant** |
|
||||
| Caster identification | Not available | Event provides `casterGuid` | **New capability** |
|
||||
| Druid mana (other units) | Not available | `GetUnitField(guid, "power1")` | **New in 7.2.0** |
|
||||
| Memory usage | ~50KB | ~200KB | 4x more (negligible) |
|
||||
|
||||
### Memory Management (7.1.0+ Fixes)
|
||||
|
||||
| Table | Before 7.1.0 | After 7.1.0 |
|
||||
|-------|--------------|-------------|
|
||||
| `lastCastRanks` | Grew indefinitely | Cleaned every 30s (>3s old) |
|
||||
| `lastFailedSpells` | Grew indefinitely | Cleaned every 30s (>2s old) |
|
||||
| `debuffSeen` (nameplates) | New table per DEBUFF_UPDATE | Reused single table |
|
||||
| `cleanedCache` (unitframes) | New table every 30s | In-place cleanup |
|
||||
|
||||
---
|
||||
|
||||
## 📋 File Changes Summary
|
||||
|
||||
### Version 7.5.0
|
||||
|
||||
| File | Location | Changes |
|
||||
|------|----------|---------|
|
||||
| `buffwatch.lua` | `modules/` | Player buff bar timer fix (UUID without slot), tooltip fix for "only own debuffs" mode |
|
||||
| `libdebuff.lua` | `libs/` | Immunity check, UnitDebuff() 8th return value `caster`, Lua 5.0 table limit workaround (11 tables to pfUI. namespace), Crash 132 fix, Player GUID caching |
|
||||
| `unitframes.lua` | `api/` | Consistent DoNothing() pattern for lightweight cooldown frames |
|
||||
| `nameplates.lua` | `modules/` | Consistent DoNothing() pattern, `childs` table reuse, indexed debuff timeout scan, quick exit optimization, Crash 132 fix |
|
||||
| `nampower.lua` | `modules/` | Crash 132 fix |
|
||||
| `superwow.lua` | `modules/` | Crash 132 fix |
|
||||
| `actionbar.lua` | `modules/` | Crash 132 fix |
|
||||
|
||||
### Version 7.4.3 (January 29, 2026)
|
||||
|
||||
| File | Location | Changes |
|
||||
|------|----------|---------|
|
||||
| `libdebuff.lua` | `libs/` | Rogue PLAYER_COMBO_POINTS fix, always use GetDuration() for CP-abilities |
|
||||
| `api.lua` | `api/` | Abbreviate() now supports 3 modes (off/2dec/1dec), 1dec always floors |
|
||||
| `config.lua` | `api/` | Added `castbardecimals` option |
|
||||
| `gui.lua` | `modules/` | Abbreviate Numbers dropdown, Castbar Timer Decimals dropdown |
|
||||
| `nameplates.lua` | `modules/` | Smooth castbar (throttle fix), countdown timer, cache cleanup |
|
||||
| `castbar.lua` | `modules/` | FormatCastbarTime() helper, respects castbardecimals config |
|
||||
|
||||
### Version 7.2.0
|
||||
|
||||
| File | Location | Changes |
|
||||
|------|----------|---------|
|
||||
| `superwow.lua` | `modules/` | Removed legacy pfDruidMana, added Target/ToT secondary mana bars, GetUnitField for all mana queries, respect Power Bar text settings |
|
||||
| `nampower.lua` | `modules/` | Major cleanup: removed dead buff tracking, HoT detection, swing timer code |
|
||||
|
||||
### Version 7.1.0
|
||||
|
||||
| File | Location | Changes |
|
||||
|------|----------|---------|
|
||||
| `libdebuff.lua` | `libs/` | Memory leak fixes for lastCastRanks, lastFailedSpells |
|
||||
| `unitframes.lua` | `api/` | In-place cache cleanup, CD frame scaling/positioning |
|
||||
| `nameplates.lua` | `modules/` | Reusable debuffSeen table, Model+CooldownFrameTemplate |
|
||||
| `cooldown.lua` | `modules/` | SetAlpha(1) for pfCooldownStyleAnimation == 1 |
|
||||
| `config.lua` | `api/` | Added nameplates.debuffanim option |
|
||||
| `gui.lua` | `modules/` | Added "Show Timer Animation" checkbox for nameplates |
|
||||
|
||||
---
|
||||
|
||||
## 📋 Installation
|
||||
|
||||
### Requirements
|
||||
|
||||
**REQUIRED:**
|
||||
- SuperWoW DLL
|
||||
- Nampower DLL
|
||||
|
||||
**Optional but Recommended:**
|
||||
- UnitXP_SP3 DLL (for accurate XP tracking)
|
||||
|
||||
### Steps
|
||||
|
||||
1. Install SuperWoW + Nampower
|
||||
2. Download pfUI Experiment build
|
||||
3. Extract to `Interface/AddOns/pfUI`
|
||||
4. `/reload`
|
||||
5. Check for errors in console
|
||||
|
||||
### Verification
|
||||
|
||||
Type `/run print(GetNampowerVersion())` - should show version number.
|
||||
|
||||
If `nil`, Nampower is not installed correctly!
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Known Issues
|
||||
|
||||
### Untested Scenarios
|
||||
|
||||
- ❌ 40-man raids with 5+ druids (slot shifting stress test)
|
||||
- ❌ Rapid target swapping with Ferocious Bite spam
|
||||
- ⚠️ Multi-caster tracking in AQ40/Naxx
|
||||
|
||||
### Edge Cases
|
||||
|
||||
1. **DEBUFF_ADDED race condition:** Sometimes fires before AURA_CAST_ON_SELF processes
|
||||
2. **Slot shifting bugs:** Complex logic for removing/adding debuffs
|
||||
3. **Combo point detection:** Relies on PLAYER_COMBO_POINTS event timing
|
||||
|
||||
---
|
||||
|
||||
## 📜 Changelog
|
||||
|
||||
### 7.5.0 (January 31, 2026)
|
||||
|
||||
**Added:**
|
||||
- ✅ UnitDebuff() 8th return value: `caster` ("player"/"other"/nil)
|
||||
- ✅ Immunity check - no timer/icon shown if debuff wasn't actually applied
|
||||
- ✅ Buff bar tooltip correctly identifies debuff slot in "only own debuffs" mode
|
||||
- ✅ `/shifttest` and `/memcheck` debug commands for libdebuff troubleshooting
|
||||
|
||||
**Fixed:**
|
||||
- 🔧 Player Buff Bar timer reset bug (UUID no longer includes slot for player buffs)
|
||||
- 🔧 Lua 5.0 local variable limit (moved 11 tables to pfUI. namespace)
|
||||
- 🔧 Crash 132 on logout (Credits: jrc13245) - affects libdebuff, nameplates, nampower, superwow, actionbar
|
||||
- 🔧 Consistent DoNothing() pattern across all cooldown frame creation
|
||||
|
||||
**Performance:**
|
||||
- ⚡ `childs` table reuse in nameplate scanner (avoids GC churn)
|
||||
- ⚡ Indexed access instead of `pairs()` for debuff timeout scanning
|
||||
- ⚡ Quick exit if not in combat for threatcolor calculation
|
||||
- ⚡ Player GUID caching in libdebuff
|
||||
|
||||
### 7.4.3 (January 29, 2026)
|
||||
|
||||
**Added:**
|
||||
- ✅ Castbar Timer Decimals setting (1 or 2 decimals)
|
||||
- ✅ Abbreviate Numbers dropdown (Full / 2 Decimals / 1 Decimal)
|
||||
- ✅ Nameplate castbar countdown (shows remaining time)
|
||||
- ✅ Cache cleanup for hidden nameplates (prevents memory leaks)
|
||||
|
||||
**Fixed:**
|
||||
- 🔧 Rogue combo point tracking (PLAYER_COMBO_POINTS was Druid-only)
|
||||
- 🔧 Kidney Shot/Rupture duration (now always uses GetDuration() for CP-abilities)
|
||||
- 🔧 Nameplate castbar stuttering (throttle only affects scanner, not updates)
|
||||
|
||||
**Changed:**
|
||||
- 🔧 Abbreviate Numbers: 1 Decimal mode always rounds DOWN (4180 → 4.1k)
|
||||
- 🔧 Nameplate castbar: counts down instead of up
|
||||
|
||||
### 7.2.0 (January 24, 2026)
|
||||
|
||||
**Added:**
|
||||
- ✅ Target Secondary Mana Bar (see Druid mana while in shapeshift form)
|
||||
- ✅ Target-of-Target Secondary Mana Bar
|
||||
- ✅ Secondary Mana Bars now respect Power Bar text settings
|
||||
|
||||
**Changed:**
|
||||
- 🔧 Secondary Mana Bars now use `GetUnitField(guid, "power1")` instead of `UnitMana()`
|
||||
- 🔧 "Show Druid Mana Bar" setting now available for ALL classes (not just Druids)
|
||||
|
||||
**Removed:**
|
||||
- ❌ Legacy `pfDruidMana` bar (replaced by `pfPlayerSecondaryMana`)
|
||||
- ❌ `UnitMana()` extended return value fallback
|
||||
- ❌ Dead code in nampower.lua: buff tracking, HoT detection, swing timer
|
||||
|
||||
### 7.1.0 (January 24, 2026)
|
||||
|
||||
**Added:**
|
||||
- ✅ Nameplate debuff timer animation support (pie/swipe effect)
|
||||
- ✅ Target frame debuff animation improvements
|
||||
- ✅ GUI option: Nameplates → Show Timer Animation
|
||||
|
||||
**Fixed:**
|
||||
- 🔧 Memory leak: `lastCastRanks` now cleaned up (>3s old entries)
|
||||
- 🔧 Memory leak: `lastFailedSpells` now cleaned up (>2s old entries)
|
||||
- 🔧 Memory churn: Reusable `debuffSeen` table in nameplates
|
||||
- 🔧 Memory churn: In-place cache cleanup in unitframes
|
||||
- 🔧 cooldown.lua: Animation now visible when pfCooldownStyleAnimation == 1
|
||||
|
||||
### 7.0.0 (January 21, 2026)
|
||||
|
||||
**Added:**
|
||||
- ✅ Event-driven debuff tracking (AURA_CAST, DEBUFF_ADDED, etc.)
|
||||
- ✅ Combo point finisher support (Rip, Rupture, Kidney Shot)
|
||||
- ✅ Carnage talent detection (Ferocious Bite refresh)
|
||||
- ✅ Debuff overwrite pairs (Faerie Fire ↔ Faerie Fire Feral)
|
||||
- ✅ Slot shifting algorithm (accurate icon placement)
|
||||
- ✅ Multi-caster tracking (multiple Moonfires)
|
||||
- ✅ Rank protection (Rank 1 can't overwrite Rank 10)
|
||||
- ✅ Unique debuff system (Hunter's Mark, Scorpid Sting)
|
||||
- ✅ Nampower GetUnitField() initial scan
|
||||
- ✅ Combat indicator fix (works on player frame now)
|
||||
|
||||
**Changed:**
|
||||
- 🔧 libdebuff.lua completely rewritten (464 → 1594 lines)
|
||||
- 🔧 UnitOwnDebuff() uses table lookup instead of tooltip scan
|
||||
|
||||
---
|
||||
## What's New in Version 6.2.6 (January 27, 2026)
|
||||
|
||||
@@ -476,7 +1416,7 @@ Same as original pfUI - free to use and modify.
|
||||
|
||||
---
|
||||
|
||||
**Version:** 6.2.6
|
||||
**Release Date:** January 27, 2026
|
||||
**Version:** 7.6.0
|
||||
**Release Date:** February 3, 2026
|
||||
**Compatibility:** Turtle WoW 1.18.0
|
||||
**Status:** Stable
|
||||
+19
-4
@@ -445,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 ]
|
||||
|
||||
+1
-1
@@ -224,6 +224,7 @@ function pfUI:LoadConfig()
|
||||
pfUI:UpdateConfig("unitframes", nil, "combowidth", "6")
|
||||
pfUI:UpdateConfig("unitframes", nil, "comboheight", "6")
|
||||
pfUI:UpdateConfig("unitframes", nil, "abbrevnum", "1")
|
||||
pfUI:UpdateConfig("unitframes", nil, "castbardecimals", "2")
|
||||
pfUI:UpdateConfig("unitframes", nil, "abbrevname", "1")
|
||||
|
||||
-- Nampower Settings
|
||||
@@ -231,7 +232,6 @@ function pfUI:LoadConfig()
|
||||
pfUI:UpdateConfig("unitframes", nil, "spellqueuesize", "24")
|
||||
pfUI:UpdateConfig("unitframes", nil, "gcd_indicator", "0")
|
||||
pfUI:UpdateConfig("unitframes", nil, "gcd_size", "4")
|
||||
pfUI:UpdateConfig("unitframes", nil, "nampower_buffs", "1")
|
||||
pfUI:UpdateConfig("unitframes", nil, "reactive_indicator", "0")
|
||||
pfUI:UpdateConfig("unitframes", nil, "reactive_size", "28")
|
||||
pfUI:UpdateConfig("unitframes", nil, "damage_tracking", "0")
|
||||
|
||||
+801
-212
File diff suppressed because it is too large
Load Diff
Vendored
+1
@@ -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"] = "显示/隐藏时间管理器",
|
||||
|
||||
+1545
-316
File diff suppressed because it is too large
Load Diff
+297
-53
@@ -12,10 +12,27 @@ setfenv(1, pfUI:GetEnvironment())
|
||||
-- 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 = {}, {}, {}, {}
|
||||
|
||||
@@ -98,6 +115,7 @@ 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
|
||||
@@ -105,12 +123,23 @@ if superwow_active then
|
||||
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
|
||||
-- Ignore own messages (sender receives own addon messages)
|
||||
local playerName = UnitName("player")
|
||||
if arg4 == playerName then return end
|
||||
|
||||
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
|
||||
@@ -174,23 +203,38 @@ libpredict:SetScript("OnEvent", function()
|
||||
duration = renewDuration or 15
|
||||
end
|
||||
|
||||
if libpredict.debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ffff[UNIT_CASTEVENT]|r spell=%s target=%s dur=%s", hotType, targetName, tostring(duration)))
|
||||
-- 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
|
||||
|
||||
-- Sende HoT
|
||||
local playerName = UnitName("player")
|
||||
libpredict:Hot(playerName, targetName, hotType, duration, nil, "UNIT_CASTEVENT")
|
||||
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 HealComm Nachricht (sender könnte noch nicht existieren bei sehr frühem Event)
|
||||
-- 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 .. "/")
|
||||
libpredict.sender:SendHealCommMsg(hotType .. "/" .. targetName .. "/" .. duration .. "/" .. rankStr .. "/")
|
||||
else
|
||||
-- Fallback: direkt senden
|
||||
local msg = hotType .. "/" .. targetName .. "/" .. duration .. "/"
|
||||
SendAddonMessage("HealComm", msg, "PARTY")
|
||||
SendAddonMessage("HealComm", msg, "RAID")
|
||||
SendAddonMessage("HealComm", msg, "BATTLEGROUND")
|
||||
-- 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)
|
||||
@@ -210,10 +254,14 @@ libpredict:SetScript("OnUpdate", function()
|
||||
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 == "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
|
||||
@@ -242,6 +290,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
|
||||
@@ -271,7 +326,7 @@ 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
|
||||
@@ -279,10 +334,10 @@ 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
|
||||
@@ -344,10 +399,10 @@ function libpredict:ParseChatMessage(sender, msg, comm)
|
||||
local delay = (heal == "Regr") and 0.3 or 0
|
||||
local correctedStart = now - delay
|
||||
|
||||
libpredict:Hot(sender, target, heal, time, correctedStart, "ParseComm-Self")
|
||||
libpredict:Hot(sender, target, heal, time, correctedStart, "ParseComm-Self", rank)
|
||||
return
|
||||
end
|
||||
libpredict:Hot(sender, target, heal, time, nil, "ParseComm")
|
||||
libpredict:Hot(sender, target, heal, time, nil, "ParseComm", rank)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -371,7 +426,7 @@ end
|
||||
-- Debug flag
|
||||
libpredict.debug = false
|
||||
|
||||
function libpredict:Hot(sender, target, spell, duration, startTime, source)
|
||||
function libpredict:Hot(sender, target, spell, duration, startTime, source, rank)
|
||||
hots[target] = hots[target] or {}
|
||||
hots[target][spell] = hots[target][spell] or {}
|
||||
|
||||
@@ -382,10 +437,30 @@ function libpredict:Hot(sender, target, spell, duration, startTime, source)
|
||||
|
||||
-- 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 = startTime or now
|
||||
hots[target][spell].rank = rank -- Store rank for protection
|
||||
|
||||
-- Debug
|
||||
if libpredict.debug then
|
||||
@@ -393,7 +468,8 @@ function libpredict:Hot(sender, target, spell, duration, startTime, source)
|
||||
" | sender=" .. (sender or "nil") ..
|
||||
" | target=" .. (target or "nil") ..
|
||||
" | spell=" .. (spell or "nil") ..
|
||||
" | dur=" .. tostring(duration) .. " (" .. type(duration) .. ")")
|
||||
" | dur=" .. tostring(duration) .. " (" .. type(duration) .. ")" ..
|
||||
" | rank=" .. tostring(rank or "?"))
|
||||
end
|
||||
|
||||
-- update aura events of relevant unitframes
|
||||
@@ -447,9 +523,10 @@ 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
|
||||
@@ -468,8 +545,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
|
||||
@@ -587,6 +665,12 @@ hooksecurefunc("CastSpell", function(id, bookType)
|
||||
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
|
||||
@@ -603,10 +687,11 @@ hooksecurefunc("CastSpell", function(id, bookType)
|
||||
instantHotCooldown[key] = now
|
||||
|
||||
if libpredict.debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[CastSpell REJU INSTANT]|r target=%s (Fallback)", target))
|
||||
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")
|
||||
libpredict.sender:SendHealCommMsg("Reju/"..target.."/"..rejuvDuration.."/")
|
||||
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()
|
||||
@@ -619,10 +704,11 @@ hooksecurefunc("CastSpell", function(id, bookType)
|
||||
instantHotCooldown[key] = now
|
||||
|
||||
if libpredict.debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[CastSpell RENEW INSTANT]|r target=%s (Fallback)", target))
|
||||
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")
|
||||
libpredict.sender:SendHealCommMsg("Renew/"..target.."/"..renewDuration.."/")
|
||||
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)
|
||||
|
||||
@@ -639,6 +725,12 @@ 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
|
||||
|
||||
-- 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
|
||||
@@ -663,10 +755,11 @@ hooksecurefunc("CastSpellByName", function(effect, target)
|
||||
instantHotCooldown[key] = now
|
||||
|
||||
if libpredict.debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[CastSpellByName REJU INSTANT]|r target=%s (Fallback)", hotTarget))
|
||||
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")
|
||||
libpredict.sender:SendHealCommMsg("Reju/"..hotTarget.."/"..rejuvDuration.."/")
|
||||
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()
|
||||
@@ -679,10 +772,11 @@ hooksecurefunc("CastSpellByName", function(effect, target)
|
||||
instantHotCooldown[key] = now
|
||||
|
||||
if libpredict.debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[CastSpellByName RENEW INSTANT]|r target=%s (Fallback)", hotTarget))
|
||||
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")
|
||||
libpredict.sender:SendHealCommMsg("Renew/"..hotTarget.."/"..renewDuration.."/")
|
||||
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)
|
||||
|
||||
@@ -697,6 +791,12 @@ hooksecurefunc("UseAction", function(slot, target, selfcast)
|
||||
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
|
||||
@@ -713,10 +813,11 @@ hooksecurefunc("UseAction", function(slot, target, selfcast)
|
||||
instantHotCooldown[key] = now
|
||||
|
||||
if libpredict.debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[UseAction REJU INSTANT]|r target=%s (Fallback)", hotTarget))
|
||||
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")
|
||||
libpredict.sender:SendHealCommMsg("Reju/"..hotTarget.."/"..rejuvDuration.."/")
|
||||
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()
|
||||
@@ -729,24 +830,39 @@ hooksecurefunc("UseAction", function(slot, target, selfcast)
|
||||
instantHotCooldown[key] = now
|
||||
|
||||
if libpredict.debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[UseAction RENEW INSTANT]|r target=%s (Fallback)", hotTarget))
|
||||
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")
|
||||
libpredict.sender:SendHealCommMsg("Renew/"..hotTarget.."/"..renewDuration.."/")
|
||||
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, "PARTY")
|
||||
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, "PARTY")
|
||||
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()
|
||||
@@ -756,15 +872,19 @@ libpredict.sender:SetScript("OnUpdate", function()
|
||||
local target = this.regrowth_target or player
|
||||
local duration = 20
|
||||
local startTime = this.regrowth_start
|
||||
local rank = this.regrowth_rank
|
||||
|
||||
libpredict:Hot(player, target, "Regr", duration, startTime, "OnUpdate")
|
||||
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)
|
||||
@@ -776,6 +896,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")
|
||||
@@ -819,10 +951,17 @@ libpredict.sender:SetScript("OnEvent", function()
|
||||
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.match(fullSpell, "Rank (%d+)") or nil
|
||||
local rankNum = rankStr and tonumber(rankStr) or nil
|
||||
|
||||
if this.regrowth_timer then
|
||||
this.regrowth_target_next = spell_queue[3]
|
||||
this.regrowth_rank_next = rankNum
|
||||
else
|
||||
this.regrowth_target = spell_queue[3]
|
||||
this.regrowth_rank = rankNum
|
||||
end
|
||||
end
|
||||
|
||||
@@ -852,6 +991,11 @@ libpredict.sender:SetScript("OnEvent", function()
|
||||
elseif event == "SPELLCAST_FAILED" or event == "SPELLCAST_INTERRUPTED" then
|
||||
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 (via " .. event .. ") to group")
|
||||
end
|
||||
libpredict.sender:SendHealCommMsg("HealStop")
|
||||
libpredict.sender.healing = nil
|
||||
elseif libpredict.sender.resurrecting then
|
||||
@@ -871,6 +1015,35 @@ libpredict.sender:SetScript("OnEvent", function()
|
||||
-- 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 = 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)
|
||||
@@ -900,7 +1073,37 @@ 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 now = pfUI.uf.now or GetTime()
|
||||
|
||||
@@ -915,6 +1118,11 @@ function libpredict:GetHotDuration(unit, spell)
|
||||
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
|
||||
@@ -922,4 +1130,40 @@ function libpredict:GetHotDuration(unit, spell)
|
||||
return start, duration, timeleft
|
||||
end
|
||||
|
||||
-- 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
|
||||
|
||||
+15
-3
@@ -668,7 +668,10 @@ pfUI:RegisterModule("actionbar", "vanilla", 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
|
||||
@@ -982,7 +985,16 @@ pfUI:RegisterModule("actionbar", "vanilla", function ()
|
||||
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
|
||||
@@ -1230,8 +1242,8 @@ pfUI:RegisterModule("actionbar", "vanilla", 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
|
||||
|
||||
+116
-26
@@ -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
@@ -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)
|
||||
|
||||
+14
-3
@@ -6,6 +6,17 @@ pfUI:RegisterModule("castbar", "vanilla", function ()
|
||||
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)
|
||||
|
||||
@@ -165,10 +176,10 @@ pfUI:RegisterModule("castbar", "vanilla", function ()
|
||||
|
||||
if this.showtimer then
|
||||
if this.delay and this.delay > 0 then
|
||||
local delay = "|cffffaaaa" .. (channel and "-" or "+") .. round(this.delay,2) .. " |r "
|
||||
this.bar.right:SetText(delay .. string.format("%.2f",cur) .. " / " .. round(max,2))
|
||||
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("%.2f",cur) .. " / " .. round(max,2))
|
||||
this.bar.right:SetText(FormatCastbarTime(cur) .. " / " .. FormatCastbarTime(max))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+23
-10
@@ -891,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"],
|
||||
@@ -1378,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://github.com/me0wg4ming/pfUI")
|
||||
end)
|
||||
SkinButton(donate)
|
||||
|
||||
@@ -1388,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)
|
||||
|
||||
@@ -1398,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)
|
||||
@@ -1496,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")
|
||||
@@ -1669,7 +1679,6 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
|
||||
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["Enhanced Buff Tracking"], C.unitframes, "nampower_buffs", "checkbox", 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" )
|
||||
@@ -1831,7 +1840,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)
|
||||
@@ -2032,7 +2041,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")
|
||||
@@ -2357,7 +2368,8 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
|
||||
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(nil, T["Show Timer Animation"], C.nameplates, "debuffanim", "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")
|
||||
@@ -2448,7 +2460,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")
|
||||
@@ -2465,4 +2478,4 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
@@ -1,6 +1,6 @@
|
||||
pfUI:RegisterModule("macrotweak", "vanilla", function ()
|
||||
-- disable macrotweak when SuperCleveRoidMacros is loaded
|
||||
if IsAddOnLoaded("SuperCleveRoidMacros") then return end
|
||||
-- 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
|
||||
|
||||
+469
-240
@@ -4,7 +4,6 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
|
||||
-- check for SuperWoW support (use SUPERWOW_VERSION global)
|
||||
local superwow_active = SUPERWOW_VERSION ~= nil
|
||||
--print("pfUI Nameplates: SuperWoW " .. (superwow_active and "ACTIVE" or "INACTIVE"))
|
||||
|
||||
-- Local function references for performance
|
||||
local GetTime = GetTime
|
||||
@@ -19,6 +18,10 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
local UnitCanAssist = UnitCanAssist
|
||||
local UnitCastingInfo = UnitCastingInfo
|
||||
local UnitChannelInfo = UnitChannelInfo
|
||||
local UnitHealth = UnitHealth
|
||||
local UnitHealthMax = UnitHealthMax
|
||||
local UnitMana = UnitMana
|
||||
local UnitManaMax = UnitManaMax
|
||||
local pairs = pairs
|
||||
local tonumber = tonumber
|
||||
local strlower = strlower
|
||||
@@ -27,6 +30,7 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
local floor = floor
|
||||
local ceil = ceil
|
||||
local abs = abs
|
||||
local mathmod = math.mod
|
||||
|
||||
local unitcolors = {
|
||||
["ENEMY_NPC"] = { .9, .2, .3, .8 },
|
||||
@@ -56,7 +60,8 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
}
|
||||
|
||||
-- catch all nameplates
|
||||
local childs, regions, plate
|
||||
local childs = {} -- PERF: Reuse table instead of creating new one each scan
|
||||
local regions, plate
|
||||
local initialized = 0
|
||||
|
||||
-- Friendly zone nameplate disable state
|
||||
@@ -66,31 +71,111 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
local parentcount = 0
|
||||
local platecount = 0
|
||||
local registry = {}
|
||||
-- NEW: Cast event cache like Overhead.lua
|
||||
local CastEvents = {}
|
||||
|
||||
-- ============================================================================
|
||||
-- OPTIMIZATION: GUID-based registries for O(1) lookups
|
||||
-- ============================================================================
|
||||
local guidRegistry = {} -- guid -> plate (for direct event routing)
|
||||
|
||||
-- Helper function to safely access libdebuff cast data
|
||||
local function GetCastInfo(guid)
|
||||
return pfUI.libdebuff_casts and pfUI.libdebuff_casts[guid]
|
||||
end
|
||||
|
||||
local debuffCache = {} -- guid -> { [spellID] = { start, duration } }
|
||||
local threatMemory = {} -- guid -> true if mob had player targeted
|
||||
local debuffSeen = {} -- reusable table for debuff tracking (avoid GC churn)
|
||||
|
||||
-- PERF: Track visible plate count for adaptive throttling
|
||||
local visiblePlateCount = 0
|
||||
local lastVisibleCheck = 0
|
||||
|
||||
-- wipe polyfill
|
||||
local wipe = wipe or function(t) for k in pairs(t) do t[k] = nil end end
|
||||
|
||||
-- Player GUID for filtering
|
||||
local _, PlayerGUID = UnitExists("player")
|
||||
|
||||
-- ============================================================================
|
||||
-- OPTIMIZATION: Config caching
|
||||
-- ============================================================================
|
||||
local cfg = {}
|
||||
local function CacheConfig()
|
||||
cfg.showcastbar = C.nameplates["showcastbar"] == "1"
|
||||
cfg.targetcastbar = C.nameplates["targetcastbar"] == "1"
|
||||
cfg.notargalpha = tonumber(C.nameplates.notargalpha) or 0.5
|
||||
if cfg.notargalpha > 1 then cfg.notargalpha = cfg.notargalpha / 100 end
|
||||
cfg.namefightcolor = C.nameplates.namefightcolor == "1"
|
||||
cfg.spellname = C.nameplates.spellname == "1"
|
||||
cfg.showhp = C.nameplates.showhp == "1"
|
||||
cfg.showdebuffs = C.nameplates["showdebuffs"] == "1"
|
||||
cfg.targetzoom = C.nameplates.targetzoom == "1"
|
||||
cfg.zoomval = (tonumber(C.nameplates.targetzoomval) or 0.4) + 1
|
||||
cfg.width = tonumber(C.nameplates.width) or 120
|
||||
cfg.heighthealth = tonumber(C.nameplates.heighthealth) or 8
|
||||
cfg.targetglow = C.nameplates.targetglow == "1"
|
||||
cfg.targethighlight = C.nameplates.targethighlight == "1"
|
||||
cfg.outcombatstate = C.nameplates.outcombatstate == "1"
|
||||
cfg.barcombatstate = C.nameplates.barcombatstate == "1"
|
||||
cfg.ccombatcasting = C.nameplates.ccombatcasting == "1"
|
||||
cfg.ccombatthreat = C.nameplates.ccombatthreat == "1"
|
||||
cfg.ccombatnothreat = C.nameplates.ccombatnothreat == "1"
|
||||
cfg.ccombatstun = C.nameplates.ccombatstun == "1"
|
||||
cfg.ccombatofftank = C.nameplates.ccombatofftank == "1"
|
||||
cfg.use_unitfonts = C.nameplates.use_unitfonts == "1"
|
||||
cfg.font_size = cfg.use_unitfonts and C.global.font_unit_size or C.global.font_size
|
||||
cfg.hptextformat = C.nameplates.hptextformat
|
||||
-- NEW: Cache debuff config
|
||||
cfg.debufftimers = C.nameplates.debufftimers == "1"
|
||||
cfg.debuffanim = tonumber(C.nameplates.debuffanim) or 0
|
||||
cfg.debufftext = tonumber(C.nameplates.debufftext) or 1
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- OPTIMIZATION: Frame state cache
|
||||
-- ============================================================================
|
||||
local frameState = {
|
||||
now = 0,
|
||||
hasTarget = false,
|
||||
targetGuid = nil,
|
||||
hasMouseover = false,
|
||||
}
|
||||
|
||||
-- cache default border color
|
||||
local er, eg, eb, ea = GetStringColor(pfUI_config.appearance.border.color)
|
||||
|
||||
local function GetCombatStateColor(guid)
|
||||
-- PERF: Quick exit if not in combat
|
||||
if not UnitAffectingCombat("player") then return false end
|
||||
if not UnitAffectingCombat(guid) then return false end
|
||||
if UnitCanAssist("player", guid) then return false end
|
||||
|
||||
local target = guid.."target"
|
||||
local color = false
|
||||
|
||||
if UnitAffectingCombat("player") and UnitAffectingCombat(guid) and not UnitCanAssist("player", guid) then
|
||||
if C.nameplates.ccombatcasting == "1" and (UnitCastingInfo(guid) or UnitChannelInfo(guid)) then
|
||||
color = combatstate.CASTING
|
||||
elseif C.nameplates.ccombatthreat == "1" and UnitIsUnit(target, "player") then
|
||||
color = combatstate.THREAT
|
||||
elseif C.nameplates.ccombatofftank == "1" and UnitName(target) and offtanks[strlower(UnitName(target))] then
|
||||
color = combatstate.OFFTANK
|
||||
elseif C.nameplates.ccombatofftank == "1" and pfUI.uf and pfUI.uf.raid and pfUI.uf.raid.tankrole[UnitName(target)] then
|
||||
color = combatstate.OFFTANK
|
||||
elseif C.nameplates.ccombatnothreat == "1" and UnitExists(target) then
|
||||
color = combatstate.NOTHREAT
|
||||
elseif C.nameplates.ccombatstun == "1" and not UnitExists(target) and not UnitIsPlayer(guid) then
|
||||
color = combatstate.STUN
|
||||
end
|
||||
local castInfo = GetCastInfo(guid)
|
||||
local isCasting = castInfo and castInfo.endTime and frameState.now < castInfo.endTime
|
||||
local targetingPlayer = UnitIsUnit(target, "player")
|
||||
|
||||
-- Remember if mob targets player, clear only when targeting someone else while NOT casting
|
||||
if targetingPlayer then
|
||||
threatMemory[guid] = true
|
||||
elseif UnitExists(target) and not isCasting then
|
||||
threatMemory[guid] = nil
|
||||
end
|
||||
|
||||
if cfg.ccombatcasting and isCasting then
|
||||
color = combatstate.CASTING
|
||||
elseif cfg.ccombatthreat and (targetingPlayer or threatMemory[guid]) then
|
||||
color = combatstate.THREAT
|
||||
elseif cfg.ccombatofftank and UnitName(target) and offtanks[strlower(UnitName(target))] then
|
||||
color = combatstate.OFFTANK
|
||||
elseif cfg.ccombatofftank and pfUI.uf and pfUI.uf.raid and pfUI.uf.raid.tankrole[UnitName(target)] then
|
||||
color = combatstate.OFFTANK
|
||||
elseif cfg.ccombatnothreat and UnitExists(target) then
|
||||
color = combatstate.NOTHREAT
|
||||
elseif cfg.ccombatstun and not UnitExists(target) and not UnitIsPlayer(guid) then
|
||||
color = combatstate.STUN
|
||||
end
|
||||
|
||||
return color
|
||||
@@ -298,31 +383,25 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
plate.debuffs[index].stacks:SetJustifyV("BOTTOM")
|
||||
plate.debuffs[index].stacks:SetTextColor(1,1,0)
|
||||
|
||||
-- Read config for cooldown animation and text
|
||||
local cooldown_anim = tonumber(C.nameplates.debuffanim) or 0
|
||||
local cooldown_text = tonumber(C.nameplates.debufftext) or 1
|
||||
|
||||
if pfUI.client <= 11200 then
|
||||
-- Animation enabled: Use Model frame with CooldownFrameTemplate
|
||||
plate.debuffs[index].cd = CreateFrame(COOLDOWN_FRAME_TYPE, plate.platename.."Debuff"..index.."Cooldown", plate.debuffs[index], "CooldownFrameTemplate")
|
||||
plate.debuffs[index].cd:SetAllPoints(plate.debuffs[index])
|
||||
plate.debuffs[index].cd.pfCooldownStyleAnimation = 1
|
||||
plate.debuffs[index].cd.pfCooldownStyleText = cooldown_text
|
||||
plate.debuffs[index].cd.pfCooldownType = "ALL"
|
||||
else
|
||||
-- Animation disabled: Create fake cooldown frame for performance (like ShaguPlates)
|
||||
-- PERF: Use lightweight fake cooldown frame when animation disabled
|
||||
-- The Model-based CooldownFrameTemplate causes major lag with many nameplates
|
||||
if pfUI.client <= 11200 and cfg.debuffanim ~= 1 then
|
||||
plate.debuffs[index].cd = CreateFrame("Frame", plate.platename.."Debuff"..index.."Cooldown", plate.debuffs[index])
|
||||
plate.debuffs[index].cd:SetAllPoints(plate.debuffs[index])
|
||||
|
||||
-- Add dummy functions so CooldownFrame_SetTimer doesn't crash
|
||||
plate.debuffs[index].cd:SetScript("OnUpdate", CooldownFrame_OnUpdateModel)
|
||||
plate.debuffs[index].cd.AdvanceTime = DoNothing
|
||||
plate.debuffs[index].cd.SetSequence = DoNothing
|
||||
plate.debuffs[index].cd.SetSequenceTime = DoNothing
|
||||
|
||||
plate.debuffs[index].cd.pfCooldownStyleAnimation = 0
|
||||
plate.debuffs[index].cd.pfCooldownStyleText = cooldown_text
|
||||
plate.debuffs[index].cd.pfCooldownType = "ALL"
|
||||
else
|
||||
-- Use CooldownFrameTemplate for animation or TBC+
|
||||
plate.debuffs[index].cd = CreateFrame(COOLDOWN_FRAME_TYPE, plate.platename.."Debuff"..index.."Cooldown", plate.debuffs[index], "CooldownFrameTemplate")
|
||||
plate.debuffs[index].cd:SetAllPoints(plate.debuffs[index])
|
||||
end
|
||||
|
||||
-- Set initial config flags (will be cached per-cooldown later)
|
||||
plate.debuffs[index].cd.pfCooldownStyleAnimation = cfg.debuffanim
|
||||
plate.debuffs[index].cd.pfCooldownStyleText = cfg.debufftext
|
||||
plate.debuffs[index].cd.pfCooldownType = "ALL"
|
||||
end
|
||||
|
||||
local function UpdateDebuffConfig(nameplate, i)
|
||||
@@ -356,25 +435,62 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
|
||||
nameplate.debuffs[i]:SetWidth(tonumber(C.nameplates.debuffsize))
|
||||
nameplate.debuffs[i]:SetHeight(tonumber(C.nameplates.debuffsize))
|
||||
|
||||
-- Update cooldown display settings
|
||||
if nameplate.debuffs[i].cd then
|
||||
local cooldown_text = tonumber(C.nameplates.debufftext) or 1
|
||||
local cooldown_anim = tonumber(C.nameplates.debuffanim) or 0
|
||||
nameplate.debuffs[i].cd.pfCooldownStyleText = cooldown_text
|
||||
nameplate.debuffs[i].cd.pfCooldownStyleAnimation = cooldown_anim
|
||||
|
||||
-- Update scale for TBC+
|
||||
if pfUI.client > 11200 then
|
||||
local debuffsize = tonumber(C.nameplates.debuffsize)
|
||||
local cdScale = debuffsize / 32
|
||||
nameplate.debuffs[i].cd:SetScale(cdScale)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- create nameplate core
|
||||
local nameplates = CreateFrame("Frame", "pfNameplates", UIParent)
|
||||
nameplates:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
nameplates:RegisterEvent("PLAYER_TARGET_CHANGED")
|
||||
nameplates:RegisterEvent("UNIT_COMBO_POINTS")
|
||||
nameplates:RegisterEvent("PLAYER_COMBO_POINTS")
|
||||
nameplates:RegisterEvent("UNIT_AURA")
|
||||
nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA")
|
||||
local nameplates = CreateFrame("Frame", "pfNameplates", UIParent)
|
||||
nameplates:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
nameplates:RegisterEvent("PLAYER_TARGET_CHANGED")
|
||||
nameplates:RegisterEvent("PLAYER_LOGOUT")
|
||||
nameplates:RegisterEvent("UNIT_COMBO_POINTS")
|
||||
nameplates:RegisterEvent("PLAYER_COMBO_POINTS")
|
||||
nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA")
|
||||
|
||||
-- NEW: Register cast events like Overhead.lua
|
||||
if superwow_active then
|
||||
nameplates:RegisterEvent("UNIT_CASTEVENT")
|
||||
-- Cast tracking handled by libdebuff (SPELL_START/GO/FAILED events)
|
||||
-- No local event registration needed
|
||||
|
||||
-- Callback from libdebuff when auras change (GUID-based, event-driven)
|
||||
nameplates.OnAuraUpdate = function(self, guid)
|
||||
if not guid then return end
|
||||
|
||||
-- GUID is actual GUID (0xF13000...) from SuperWoW/Nampower events
|
||||
local plate = guidRegistry[guid]
|
||||
if plate and plate.nameplate then
|
||||
-- Mark nameplate for aura update in next OnUpdate cycle
|
||||
plate.nameplate.auraUpdate = true
|
||||
end
|
||||
end
|
||||
|
||||
nameplates:SetScript("OnEvent", function()
|
||||
if event == "PLAYER_ENTERING_WORLD" or event == "ZONE_CHANGED_NEW_AREA" then
|
||||
-- Stop event handling during logout to prevent crash 132
|
||||
if event == "PLAYER_LOGOUT" then
|
||||
this:UnregisterAllEvents()
|
||||
this:SetScript("OnEvent", nil)
|
||||
this:SetScript("OnUpdate", nil)
|
||||
if nameplates.mouselook then
|
||||
nameplates.mouselook:SetScript("OnUpdate", nil)
|
||||
end
|
||||
return
|
||||
|
||||
elseif event == "PLAYER_ENTERING_WORLD" or event == "ZONE_CHANGED_NEW_AREA" then
|
||||
if event == "PLAYER_ENTERING_WORLD" then
|
||||
_, PlayerGUID = UnitExists("player")
|
||||
CacheConfig()
|
||||
this:SetGameVariables()
|
||||
end
|
||||
|
||||
@@ -419,47 +535,27 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
savedFriendlyState = nil
|
||||
end
|
||||
end
|
||||
elseif event == "UNIT_CASTEVENT" then
|
||||
-- NEW: Event-based cast handling from Overhead.lua
|
||||
local casterGUID = arg1
|
||||
if casterGUID == PlayerGUID then return end
|
||||
|
||||
local eventType = arg3 -- "START", "CAST", "FAIL", "CHANNEL"
|
||||
local spellID = arg4
|
||||
local castDuration = arg5
|
||||
local spellName, _, icon = SpellInfo(spellID)
|
||||
|
||||
if eventType == "MAINHAND" or eventType == "OFFHAND" then
|
||||
return
|
||||
end
|
||||
|
||||
if eventType == "CAST" then
|
||||
if not CastEvents[casterGUID] or spellID ~= CastEvents[casterGUID].spell then
|
||||
return
|
||||
|
||||
elseif event == "PLAYER_TARGET_CHANGED" then
|
||||
-- Flag target plate for update via GUID registry
|
||||
local _, targetGuid = UnitExists("target")
|
||||
if targetGuid then
|
||||
local plate = guidRegistry[targetGuid]
|
||||
if plate and plate.nameplate then
|
||||
plate.nameplate.targetUpdate = true
|
||||
end
|
||||
end
|
||||
|
||||
if eventType == "START" or eventType == "CHANNEL" then
|
||||
if not CastEvents[casterGUID] then
|
||||
CastEvents[casterGUID] = {}
|
||||
-- Also propagate to all plates for alpha/strata updates
|
||||
this.eventcache = true
|
||||
|
||||
elseif event == "PLAYER_COMBO_POINTS" or event == "UNIT_COMBO_POINTS" then
|
||||
-- Only flag the target plate for combo point update
|
||||
local _, targetGuid = UnitExists("target")
|
||||
if targetGuid then
|
||||
local plate = guidRegistry[targetGuid]
|
||||
if plate and plate.nameplate then
|
||||
plate.nameplate.comboUpdate = true
|
||||
end
|
||||
wipe(CastEvents[casterGUID])
|
||||
CastEvents[casterGUID].event = eventType
|
||||
CastEvents[casterGUID].spellID = spellID
|
||||
CastEvents[casterGUID].spellName = spellName
|
||||
CastEvents[casterGUID].icon = icon
|
||||
CastEvents[casterGUID].startTime = GetTime()
|
||||
CastEvents[casterGUID].endTime = castDuration and GetTime() + castDuration / 1000
|
||||
CastEvents[casterGUID].duration = castDuration and castDuration / 1000 or nil
|
||||
elseif eventType == "FAIL" then
|
||||
if CastEvents[casterGUID] then
|
||||
wipe(CastEvents[casterGUID])
|
||||
end
|
||||
end
|
||||
|
||||
-- Propagate cast event to all nameplates
|
||||
for plate in pairs(registry) do
|
||||
plate.eventcache = true
|
||||
end
|
||||
else
|
||||
this.eventcache = true
|
||||
@@ -467,8 +563,10 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
end)
|
||||
|
||||
nameplates:SetScript("OnUpdate", function()
|
||||
-- Throttle main scanner to reasonable rate
|
||||
if (this.tick or 0) > GetTime() then return else this.tick = GetTime() + 0.05 end
|
||||
-- PERF: Cache GetTime() once per frame
|
||||
frameState.now = GetTime()
|
||||
frameState.hasTarget, frameState.targetGuid = UnitExists("target")
|
||||
frameState.hasMouseover = UnitExists("mouseover")
|
||||
|
||||
-- propagate events to all nameplates
|
||||
if this.eventcache then
|
||||
@@ -478,19 +576,76 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
end
|
||||
end
|
||||
|
||||
-- detect new nameplates
|
||||
parentcount = WorldFrame:GetNumChildren()
|
||||
if initialized < parentcount then
|
||||
childs = { WorldFrame:GetChildren() }
|
||||
for i = initialized + 1, parentcount do
|
||||
plate = childs[i]
|
||||
if IsNamePlate(plate) and not registry[plate] then
|
||||
nameplates.OnCreate(plate)
|
||||
registry[plate] = plate
|
||||
-- PERF: Update visible plate count periodically for adaptive throttling
|
||||
if frameState.now - lastVisibleCheck > 0.5 then
|
||||
lastVisibleCheck = frameState.now
|
||||
local count = 0
|
||||
for plate in pairs(registry) do
|
||||
if plate:IsVisible() then count = count + 1 end
|
||||
end
|
||||
visiblePlateCount = count
|
||||
end
|
||||
|
||||
-- Throttle ONLY the nameplate scanner
|
||||
local scanThrottle = nameplates.combat and nameplates.combat.inCombat and 0.1 or 0.05
|
||||
local shouldScan = (this.tick or 0) <= frameState.now
|
||||
if shouldScan then
|
||||
this.tick = frameState.now + scanThrottle
|
||||
|
||||
-- detect new nameplates
|
||||
parentcount = WorldFrame:GetNumChildren()
|
||||
if initialized < parentcount then
|
||||
-- PERF: Reuse childs table instead of creating new one
|
||||
local newchilds = { WorldFrame:GetChildren() }
|
||||
for i = 1, parentcount do
|
||||
childs[i] = newchilds[i]
|
||||
end
|
||||
|
||||
for i = initialized + 1, parentcount do
|
||||
plate = childs[i]
|
||||
if IsNamePlate(plate) and not registry[plate] then
|
||||
nameplates.OnCreate(plate)
|
||||
registry[plate] = plate
|
||||
end
|
||||
end
|
||||
|
||||
initialized = parentcount
|
||||
end
|
||||
end
|
||||
|
||||
-- Central OnUpdate for all visible plates
|
||||
for plate in pairs(registry) do
|
||||
if plate:IsVisible() then
|
||||
nameplates.OnUpdate(plate, frameState)
|
||||
else
|
||||
-- PERF: Clean up ALL caches for hidden plates to prevent memory leak
|
||||
local guid = plate.nameplate and plate.nameplate.cachedGuid
|
||||
if guid then
|
||||
-- Remove from guidRegistry
|
||||
if guidRegistry[guid] == plate then
|
||||
guidRegistry[guid] = nil
|
||||
end
|
||||
|
||||
-- Clean cast cache ONLY if cast has expired
|
||||
-- (Don't delete active casts just because plate was hidden briefly)
|
||||
local castInfo = GetCastInfo(guid)
|
||||
if castInfo and castInfo.endTime and castInfo.endTime < frameState.now then
|
||||
if pfUI.libdebuff_casts then
|
||||
pfUI.libdebuff_casts[guid] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Clean debuffCache
|
||||
if debuffCache[guid] then
|
||||
debuffCache[guid] = nil
|
||||
end
|
||||
|
||||
-- Clean threatMemory
|
||||
if threatMemory[guid] then
|
||||
threatMemory[guid] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
initialized = parentcount
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -498,13 +653,22 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
nameplates.combat = CreateFrame("Frame")
|
||||
nameplates.combat:RegisterEvent("PLAYER_ENTER_COMBAT")
|
||||
nameplates.combat:RegisterEvent("PLAYER_LEAVE_COMBAT")
|
||||
nameplates.combat:RegisterEvent("PLAYER_LOGOUT")
|
||||
nameplates.combat:SetScript("OnEvent", function()
|
||||
if event == "PLAYER_ENTER_COMBAT" then
|
||||
if event == "PLAYER_LOGOUT" then
|
||||
this:UnregisterAllEvents()
|
||||
this:SetScript("OnEvent", nil)
|
||||
return
|
||||
elseif event == "PLAYER_ENTER_COMBAT" then
|
||||
this.inCombat = 1
|
||||
if PlayerFrame then PlayerFrame.inCombat = 1 end
|
||||
elseif event == "PLAYER_LEAVE_COMBAT" then
|
||||
this.inCombat = nil
|
||||
if PlayerFrame then PlayerFrame.inCombat = nil end
|
||||
-- Clear threat memory when leaving combat
|
||||
for k in pairs(threatMemory) do
|
||||
threatMemory[k] = nil
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -635,9 +799,13 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
nameplate.castbar = castbar
|
||||
end
|
||||
|
||||
-- Stagger tick to spread updates across frames (0.05s apart per plate)
|
||||
nameplate.tick = GetTime() + mathmod(platecount, 10) * 0.05
|
||||
|
||||
parent.nameplate = nameplate
|
||||
HookScript(parent, "OnShow", nameplates.OnShow)
|
||||
HookScript(parent, "OnUpdate", nameplates.OnUpdate)
|
||||
-- NOTE: OnUpdate is now handled centrally, not per-plate/
|
||||
parent:SetScript("OnUpdate", nil) -- Disable Blizzard's OnUpdate
|
||||
|
||||
nameplates.OnConfigChange(parent)
|
||||
nameplates.OnShow(parent)
|
||||
@@ -808,12 +976,12 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
-- always make sure to keep plate visible
|
||||
plate:Show()
|
||||
|
||||
if target and C.nameplates.targetglow == "1" then
|
||||
if target and cfg.targetglow then
|
||||
plate.glow:Show() else plate.glow:Hide()
|
||||
end
|
||||
|
||||
-- target indicator
|
||||
if superwow_active and C.nameplates.outcombatstate == "1" then
|
||||
if superwow_active and cfg.outcombatstate then
|
||||
local guid = plate.parent:GetName(1) or ""
|
||||
|
||||
-- determine color based on combat state
|
||||
@@ -822,7 +990,7 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
|
||||
-- set border color
|
||||
plate.health.backdrop:SetBackdropBorderColor(color.r, color.g, color.b, color.a)
|
||||
elseif target and C.nameplates.targethighlight == "1" then
|
||||
elseif target and cfg.targethighlight then
|
||||
plate.health.backdrop:SetBackdropBorderColor(plate.health.hlr, plate.health.hlg, plate.health.hlb, plate.health.hla)
|
||||
elseif C.nameplates.outfriendlynpc == "1" and unittype == "FRIENDLY_NPC" then
|
||||
plate.health.backdrop:SetBackdropBorderColor(unpack(unitcolors[unittype]))
|
||||
@@ -899,30 +1067,44 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
plate.health:SetMinMaxValues(hpmin, hpmax)
|
||||
plate.health:SetValue(hp)
|
||||
|
||||
if C.nameplates.showhp == "1" then
|
||||
if cfg.showhp then
|
||||
local rhp, rhpmax, estimated
|
||||
if hpmax > 100 or (round(hpmax/100*hp) ~= hp) then
|
||||
rhp, rhpmax = hp, hpmax
|
||||
elseif pfUI.libhealth and pfUI.libhealth.enabled then
|
||||
rhp, rhpmax, estimated = pfUI.libhealth:GetUnitHealthByName(name,level,tonumber(hp),tonumber(hpmax))
|
||||
|
||||
-- Try Nampower first for real HP values via GUID
|
||||
local guid = superwow_active and plate.parent:GetName(1) or nil
|
||||
if guid and GetUnitField then
|
||||
local npHp = GetUnitField(guid, "health")
|
||||
local npMaxHp = GetUnitField(guid, "maxHealth")
|
||||
if npHp and npHp > 0 and npMaxHp and npMaxHp > 0 then
|
||||
rhp, rhpmax = npHp, npMaxHp
|
||||
end
|
||||
end
|
||||
|
||||
-- Fallback to existing methods
|
||||
if not rhp then
|
||||
if hpmax > 100 or (round(hpmax/100*hp) ~= hp) then
|
||||
rhp, rhpmax = hp, hpmax
|
||||
elseif pfUI.libhealth and pfUI.libhealth.enabled then
|
||||
rhp, rhpmax, estimated = pfUI.libhealth:GetUnitHealthByName(name,level,tonumber(hp),tonumber(hpmax))
|
||||
end
|
||||
end
|
||||
|
||||
local setting = C.nameplates.hptextformat
|
||||
local hasdata = ( estimated or hpmax > 100 or (round(hpmax/100*hp) ~= hp) )
|
||||
local setting = cfg.hptextformat
|
||||
local hasdata = ( rhp and rhpmax ) or estimated or hpmax > 100 or (round(hpmax/100*hp) ~= hp)
|
||||
|
||||
if setting == "curperc" and hasdata then
|
||||
if setting == "curperc" and hasdata and rhp then
|
||||
plate.health.text:SetText(string.format("%s | %s%%", Abbreviate(rhp), ceil(hp/hpmax*100)))
|
||||
elseif setting == "cur" and hasdata then
|
||||
elseif setting == "cur" and hasdata and rhp then
|
||||
plate.health.text:SetText(string.format("%s", Abbreviate(rhp)))
|
||||
elseif setting == "curmax" and hasdata then
|
||||
elseif setting == "curmax" and hasdata and rhp then
|
||||
plate.health.text:SetText(string.format("%s - %s", Abbreviate(rhp), Abbreviate(rhpmax)))
|
||||
elseif setting == "curmaxs" and hasdata then
|
||||
elseif setting == "curmaxs" and hasdata and rhp then
|
||||
plate.health.text:SetText(string.format("%s / %s", Abbreviate(rhp), Abbreviate(rhpmax)))
|
||||
elseif setting == "curmaxperc" and hasdata then
|
||||
elseif setting == "curmaxperc" and hasdata and rhp then
|
||||
plate.health.text:SetText(string.format("%s - %s | %s%%", Abbreviate(rhp), Abbreviate(rhpmax), ceil(hp/hpmax*100)))
|
||||
elseif setting == "curmaxpercs" and hasdata then
|
||||
elseif setting == "curmaxpercs" and hasdata and rhp then
|
||||
plate.health.text:SetText(string.format("%s / %s | %s%%", Abbreviate(rhp), Abbreviate(rhpmax), ceil(hp/hpmax*100)))
|
||||
elseif setting == "deficit" then
|
||||
elseif setting == "deficit" and rhp then
|
||||
plate.health.text:SetText(string.format("-%s" .. (hasdata and "" or "%%"), Abbreviate(rhpmax - rhp)))
|
||||
else -- "percent" as fallback
|
||||
plate.health.text:SetText(string.format("%s%%", ceil(hp/hpmax*100)))
|
||||
@@ -943,7 +1125,7 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
r, g, b, a = .5, .5, .5, .8
|
||||
end
|
||||
|
||||
if superwow_active and C.nameplates.barcombatstate == "1" then
|
||||
if superwow_active and cfg.barcombatstate then
|
||||
local guid = plate.parent:GetName(1) or ""
|
||||
local color = GetCombatStateColor(guid)
|
||||
|
||||
@@ -971,7 +1153,7 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
-- update debuffs
|
||||
local index = 1
|
||||
|
||||
if C.nameplates["showdebuffs"] == "1" then
|
||||
if cfg.showdebuffs then
|
||||
local verify = string.format("%s:%s", (name or ""), (level or ""))
|
||||
|
||||
-- update cached debuffs
|
||||
@@ -1008,18 +1190,26 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
plate.debuffs[index].stacks:Hide()
|
||||
end
|
||||
|
||||
if duration and timeleft and C.nameplates.debufftimers == "1" then
|
||||
-- Read config values for cooldown display
|
||||
local cooldown_anim = tonumber(C.nameplates.debuffanim) or 0
|
||||
local cooldown_text = tonumber(C.nameplates.debufftext) or 1
|
||||
if duration and timeleft and cfg.debufftimers then
|
||||
-- PERF: Only update cooldown if start time changed significantly
|
||||
local cd = plate.debuffs[index].cd
|
||||
local newStart = GetTime() + timeleft - duration
|
||||
|
||||
-- Ensure cooldown flags are set
|
||||
plate.debuffs[index].cd.pfCooldownStyleText = cooldown_text
|
||||
plate.debuffs[index].cd.pfCooldownStyleAnimation = cooldown_anim
|
||||
plate.debuffs[index].cd.pfCooldownType = "ALL"
|
||||
|
||||
plate.debuffs[index].cd:Show()
|
||||
CooldownFrame_SetTimer(plate.debuffs[index].cd, GetTime() + timeleft - duration, duration, 1)
|
||||
if not cd.cachedStart or abs(cd.cachedStart - newStart) > 0.5 then
|
||||
-- Update config flags only on first run or config change
|
||||
if not cd.configCached or cd.cachedAnim ~= cfg.debuffanim or cd.cachedText ~= cfg.debufftext then
|
||||
cd.pfCooldownStyleAnimation = cfg.debuffanim
|
||||
cd.pfCooldownStyleText = cfg.debufftext
|
||||
cd:SetAlpha(cfg.debuffanim == 1 and 1 or 0)
|
||||
cd.cachedAnim = cfg.debuffanim
|
||||
cd.cachedText = cfg.debufftext
|
||||
cd.configCached = true
|
||||
end
|
||||
|
||||
cd:Show()
|
||||
CooldownFrame_SetTimer(cd, newStart, duration, 1)
|
||||
cd.cachedStart = newStart
|
||||
end
|
||||
end
|
||||
|
||||
index = index + 1
|
||||
@@ -1039,45 +1229,111 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
local frame = frame or this
|
||||
local nameplate = frame.nameplate
|
||||
|
||||
-- Register GUID when plate becomes visible
|
||||
if superwow_active then
|
||||
local guid = frame:GetName(1)
|
||||
if guid then
|
||||
nameplate.cachedGuid = guid
|
||||
guidRegistry[guid] = frame
|
||||
end
|
||||
end
|
||||
|
||||
nameplates:OnDataChanged(nameplate)
|
||||
end
|
||||
|
||||
nameplates.OnUpdate = function(frame)
|
||||
local frame = frame or this
|
||||
nameplates.OnUpdate = function(frame, state)
|
||||
local nameplate = frame.nameplate
|
||||
local now = GetTime()
|
||||
local now = state and state.now or GetTime()
|
||||
|
||||
-- Performance: Skip invisible frames immediately
|
||||
local isVisible = frame:IsVisible()
|
||||
if not isVisible then return end
|
||||
-- Update GUID registry (lightweight, needed for event routing)
|
||||
if superwow_active then
|
||||
local guid = frame:GetName(1)
|
||||
if guid and guid ~= nameplate.cachedGuid then
|
||||
if nameplate.cachedGuid and guidRegistry[nameplate.cachedGuid] == frame then
|
||||
guidRegistry[nameplate.cachedGuid] = nil
|
||||
end
|
||||
nameplate.cachedGuid = guid
|
||||
guidRegistry[guid] = frame
|
||||
end
|
||||
end
|
||||
|
||||
-- Intelligent throttling based on target/castbar status
|
||||
local target = UnitExists("target") and frame:GetAlpha() >= 0.99 or nil
|
||||
-- PERF: Intelligent throttling based on target/castbar status and plate count
|
||||
local target = state and state.hasTarget and frame:GetAlpha() >= 0.99 or nil
|
||||
local isCasting = nameplate.castbar and nameplate.castbar:IsShown()
|
||||
|
||||
local throttle
|
||||
if target or isCasting then
|
||||
throttle = 0.02 -- 50 FPS for target OR active castbar
|
||||
elseif visiblePlateCount > 20 then
|
||||
throttle = 0.15 -- ~7 FPS for mass pulls (20+ plates)
|
||||
else
|
||||
throttle = 0.1 -- 10 FPS for others (healthbar updates)
|
||||
end
|
||||
|
||||
if (nameplate.lasttick or 0) + throttle > now then return end
|
||||
-- Check for pending event updates (these bypass throttle for immediate response)
|
||||
local hasEventUpdate = nameplate.eventcache or nameplate.auraUpdate or nameplate.castUpdate or nameplate.targetUpdate or nameplate.comboUpdate
|
||||
|
||||
-- Event updates bypass throttle
|
||||
if not hasEventUpdate and (nameplate.lasttick or 0) + throttle > now then return end
|
||||
nameplate.lasttick = now
|
||||
|
||||
-- =========================================================================
|
||||
-- EVERYTHING BELOW RUNS AT THROTTLED RATE (50 FPS target, 10 FPS others)
|
||||
-- =========================================================================
|
||||
|
||||
local update
|
||||
local original = nameplate.original
|
||||
local name = original.name:GetText()
|
||||
local mouseover = UnitExists("mouseover") and original.glow:IsShown() or nil
|
||||
local namefightcolor = C.nameplates.namefightcolor == "1"
|
||||
local mouseover = state and state.hasMouseover and original.glow:IsShown() or nil
|
||||
|
||||
-- trigger queued event update
|
||||
if nameplate.eventcache then
|
||||
if hasEventUpdate then
|
||||
nameplates:OnDataChanged(nameplate)
|
||||
nameplate.eventcache = nil
|
||||
nameplate.auraUpdate = nil
|
||||
nameplate.castUpdate = nil
|
||||
nameplate.targetUpdate = nil
|
||||
nameplate.comboUpdate = nil
|
||||
end
|
||||
|
||||
-- OPTIMIZED: Cache strata changes
|
||||
-- =========================================================================
|
||||
-- VANILLA OVERLAP/CLICKTHROUGH HANDLING
|
||||
-- =========================================================================
|
||||
if pfUI.client <= 11200 then
|
||||
local useOverlap = C.nameplates["overlap"] == "1" or C.nameplates["vertical_offset"] ~= "0"
|
||||
local clickable = C.nameplates["clickthrough"] ~= "1"
|
||||
|
||||
if not clickable then
|
||||
frame:EnableMouse(false)
|
||||
nameplate:EnableMouse(false)
|
||||
else
|
||||
local plate = useOverlap and nameplate or frame
|
||||
plate:EnableMouse(clickable)
|
||||
end
|
||||
|
||||
if C.nameplates["overlap"] == "1" then
|
||||
if frame:GetWidth() > 1 then
|
||||
frame:SetWidth(1)
|
||||
frame:SetHeight(1)
|
||||
end
|
||||
else
|
||||
if not nameplate.dwidth then
|
||||
nameplate.dwidth = floor(nameplate:GetWidth() * UIParent:GetScale())
|
||||
end
|
||||
|
||||
if floor(frame:GetWidth()) ~= nameplate.dwidth then
|
||||
frame:SetWidth(nameplate:GetWidth() * UIParent:GetScale())
|
||||
frame:SetHeight(nameplate:GetHeight() * UIParent:GetScale())
|
||||
end
|
||||
end
|
||||
|
||||
local mouseEnabled = nameplate:IsMouseEnabled()
|
||||
if C.nameplates["clickthrough"] == "0" and C.nameplates["overlap"] == "1" and SpellIsTargeting() == mouseEnabled then
|
||||
nameplate:EnableMouse(not mouseEnabled)
|
||||
end
|
||||
end
|
||||
|
||||
-- Cache strata changes
|
||||
if nameplate.istarget ~= target then
|
||||
nameplate.target_strata = nil
|
||||
end
|
||||
@@ -1092,21 +1348,11 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
|
||||
nameplate.istarget = target
|
||||
|
||||
-- set non-target plate alpha (cached to prevent flicker)
|
||||
local configAlpha = tonumber(C.nameplates.notargalpha)
|
||||
if not configAlpha or configAlpha < 0 then
|
||||
configAlpha = 100
|
||||
end
|
||||
|
||||
-- Ensure we're working with 0-1 range
|
||||
if configAlpha > 1 then
|
||||
configAlpha = configAlpha / 100
|
||||
end
|
||||
|
||||
local desiredAlpha = (target or not UnitExists("target")) and 1 or configAlpha
|
||||
-- Set non-target plate alpha
|
||||
local configAlpha = cfg.notargalpha or 0.5
|
||||
local desiredAlpha = (target or not state.hasTarget) and 1 or configAlpha
|
||||
|
||||
if nameplate.cachedAlpha ~= desiredAlpha then
|
||||
-- Setze nur die nameplate Alpha, nicht den parent frame
|
||||
nameplate:SetAlpha(desiredAlpha)
|
||||
nameplate.cachedAlpha = desiredAlpha
|
||||
end
|
||||
@@ -1129,13 +1375,22 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
update = true
|
||||
end
|
||||
|
||||
-- trigger update when name color changed
|
||||
-- trigger update when name color changed (includes combat state check)
|
||||
local r, g, b = original.name:GetTextColor()
|
||||
if r + g + b ~= nameplate.cache.namecolor then
|
||||
local inCombatWithPlayer = false
|
||||
if superwow_active and cfg.namefightcolor then
|
||||
local guid = nameplate.cachedGuid
|
||||
if guid then
|
||||
inCombatWithPlayer = UnitAffectingCombat(guid) and UnitAffectingCombat("player")
|
||||
end
|
||||
end
|
||||
|
||||
if r + g + b ~= nameplate.cache.namecolor or (cfg.namefightcolor and nameplate.cache.inCombat ~= inCombatWithPlayer) then
|
||||
nameplate.cache.namecolor = r + g + b
|
||||
nameplate.cache.inCombat = inCombatWithPlayer
|
||||
|
||||
if namefightcolor then
|
||||
if r > .9 and g < .2 and b < .2 then
|
||||
if cfg.namefightcolor then
|
||||
if (r > .9 and g < .2 and b < .2) or inCombatWithPlayer then
|
||||
nameplate.name:SetTextColor(1,0.4,0.2,1)
|
||||
else
|
||||
nameplate.name:SetTextColor(r,g,b,1)
|
||||
@@ -1155,10 +1410,11 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
update = true
|
||||
end
|
||||
|
||||
-- scan for debuff timeouts
|
||||
-- PERF: scan for debuff timeouts using indexed access instead of pairs()
|
||||
if nameplate.debuffcache then
|
||||
for id, data in pairs(nameplate.debuffcache) do
|
||||
if ( not data.stop or data.stop < now ) and not data.empty then
|
||||
for id = 1, 16 do
|
||||
local data = nameplate.debuffcache[id]
|
||||
if data and ( not data.stop or data.stop < now ) and not data.empty then
|
||||
data.empty = true
|
||||
update = true
|
||||
end
|
||||
@@ -1176,12 +1432,12 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
nameplate.tick = now + .5
|
||||
end
|
||||
|
||||
-- OPTIMIZED: Cache zoom dimensions
|
||||
if target and C.nameplates.targetzoom == "1" then
|
||||
-- Zoom animation
|
||||
if target and cfg.targetzoom then
|
||||
if not nameplate.health.zoomed then
|
||||
local zoomval = tonumber(C.nameplates.targetzoomval)+1
|
||||
local wc = tonumber(C.nameplates.width)*zoomval
|
||||
local hc = tonumber(C.nameplates.heighthealth)*(zoomval*.9)
|
||||
local zoomval = cfg.zoomval
|
||||
local wc = cfg.width * zoomval
|
||||
local hc = cfg.heighthealth * (zoomval * .9)
|
||||
nameplate.health.targetWidth = wc
|
||||
nameplate.health.targetHeight = hc
|
||||
end
|
||||
@@ -1189,27 +1445,27 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
local w, h = nameplate.health:GetWidth(), nameplate.health:GetHeight()
|
||||
local wc, hc = nameplate.health.targetWidth, nameplate.health.targetHeight
|
||||
|
||||
-- Nutze kleine Toleranz um Fließkomma-Schwankungen zu vermeiden
|
||||
if wc > w + 0.5 then
|
||||
nameplate.health:SetWidth(w*1.05)
|
||||
nameplate.health.zoomTransition = true
|
||||
elseif hc > h + 0.5 then
|
||||
nameplate.health:SetHeight(h*1.05)
|
||||
nameplate.health.zoomTransition = true
|
||||
else
|
||||
if nameplate.health.zoomTransition then
|
||||
nameplate.health:SetWidth(wc)
|
||||
nameplate.health:SetHeight(hc)
|
||||
nameplate.health.zoomTransition = nil
|
||||
if wc and hc then
|
||||
if wc > w + 0.5 then
|
||||
nameplate.health:SetWidth(w*1.05)
|
||||
nameplate.health.zoomTransition = true
|
||||
elseif hc > h + 0.5 then
|
||||
nameplate.health:SetHeight(h*1.05)
|
||||
nameplate.health.zoomTransition = true
|
||||
else
|
||||
if nameplate.health.zoomTransition then
|
||||
nameplate.health:SetWidth(wc)
|
||||
nameplate.health:SetHeight(hc)
|
||||
nameplate.health.zoomTransition = nil
|
||||
end
|
||||
nameplate.health.zoomed = true
|
||||
end
|
||||
nameplate.health.zoomed = true
|
||||
end
|
||||
elseif nameplate.health.zoomed or nameplate.health.zoomTransition then
|
||||
local w, h = nameplate.health:GetWidth(), nameplate.health:GetHeight()
|
||||
local wc = tonumber(C.nameplates.width)
|
||||
local hc = tonumber(C.nameplates.heighthealth)
|
||||
local wc = cfg.width
|
||||
local hc = cfg.heighthealth
|
||||
|
||||
-- Nutze kleine Toleranz um Fließkomma-Schwankungen zu vermeiden
|
||||
if w > wc + 0.5 then
|
||||
nameplate.health:SetWidth(w*.95)
|
||||
elseif h > hc + 0.5 then
|
||||
@@ -1224,27 +1480,25 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
end
|
||||
end
|
||||
|
||||
-- OPTIMIZED: UNIT_CASTEVENT implementation
|
||||
-- OPTIMIZED: 100% Nampower - libdebuff handles all cast events (SPELL_START/GO/FAILED)
|
||||
-- Use multiple checks for target detection (target variable, istarget flag, or zoomed state)
|
||||
local isTargetPlate = target or nameplate.istarget or (nameplate.health and nameplate.health.zoomed)
|
||||
if C.nameplates["showcastbar"] == "1" and ( C.nameplates["targetcastbar"] == "0" or isTargetPlate ) then
|
||||
if cfg.showcastbar and ( not cfg.targetcastbar or isTargetPlate ) then
|
||||
local unitstr = nil
|
||||
local targetGUID = nil
|
||||
|
||||
-- Get GUID for CastEvents lookup
|
||||
if isTargetPlate and superwow_active then
|
||||
-- Get target's GUID for event cache lookup
|
||||
local _, guid = UnitExists("target")
|
||||
targetGUID = guid
|
||||
-- Get GUID for CastEvents lookup - use cached GUID when available
|
||||
if isTargetPlate then
|
||||
targetGUID = state and state.targetGuid
|
||||
end
|
||||
|
||||
-- Use SuperWoW GUID for non-target plates
|
||||
-- Use cached GUID for non-target plates
|
||||
if superwow_active and not isTargetPlate then
|
||||
unitstr = nameplate.parent:GetName(1)
|
||||
unitstr = nameplate.cachedGuid
|
||||
end
|
||||
|
||||
-- Check event-based cast cache first (use GUID)
|
||||
local castInfo = (targetGUID and CastEvents[targetGUID]) or (unitstr and CastEvents[unitstr])
|
||||
local castInfo = GetCastInfo(targetGUID) or (unitstr and GetCastInfo(unitstr))
|
||||
|
||||
if castInfo and castInfo.spellID then
|
||||
-- Check if cast is still valid
|
||||
@@ -1266,9 +1520,15 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
end
|
||||
|
||||
nameplate.castbar:SetValue(barValue)
|
||||
nameplate.castbar.text:SetText(round(now - castInfo.startTime, 1))
|
||||
-- Show remaining time (countdown), not elapsed time
|
||||
local remaining = castInfo.endTime - now
|
||||
if C.unitframes.castbardecimals == "1" then
|
||||
nameplate.castbar.text:SetText(floor(remaining * 10) / 10)
|
||||
else
|
||||
nameplate.castbar.text:SetText(string.format("%.2f", remaining))
|
||||
end
|
||||
|
||||
if C.nameplates.spellname == "1" then
|
||||
if cfg.spellname then
|
||||
nameplate.castbar.spell:SetText(castInfo.spellName)
|
||||
else
|
||||
nameplate.castbar.spell:SetText("")
|
||||
@@ -1293,6 +1553,7 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
end
|
||||
|
||||
if not cast and not channel then
|
||||
-- No cast data = hide castbar (fixes stuck castbar on FAIL for non-target plates)
|
||||
nameplate.castbar:Hide()
|
||||
else
|
||||
local effect = cast or channel
|
||||
@@ -1304,7 +1565,14 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
|
||||
nameplate.castbar:SetMinMaxValues(0, duration/1000)
|
||||
nameplate.castbar:SetValue(cur)
|
||||
nameplate.castbar.text:SetText(round(cur,1))
|
||||
-- Show remaining time (countdown), not elapsed time
|
||||
local remaining = max - cur
|
||||
if channel then remaining = cur end -- Channel already counts down
|
||||
if C.unitframes.castbardecimals == "1" then
|
||||
nameplate.castbar.text:SetText(floor(remaining * 10) / 10)
|
||||
else
|
||||
nameplate.castbar.text:SetText(string.format("%.2f", remaining))
|
||||
end
|
||||
|
||||
if C.nameplates.spellname == "1" then
|
||||
nameplate.castbar.spell:SetText(effect)
|
||||
@@ -1349,6 +1617,9 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
nameplates:SetGameVariables()
|
||||
|
||||
nameplates.UpdateConfig = function()
|
||||
-- Refresh config cache for all cfg.* values
|
||||
CacheConfig()
|
||||
|
||||
-- update debuff filters
|
||||
DebuffFilterPopulate()
|
||||
|
||||
@@ -1458,48 +1729,6 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
|
||||
end
|
||||
end
|
||||
|
||||
local hookOnUpdate = nameplates.OnUpdate
|
||||
nameplates.OnUpdate = function(self)
|
||||
-- initialize shortcut variables
|
||||
local plate = (C.nameplates["overlap"] == "1" or C.nameplates["vertical_offset"] ~= "0") and this.nameplate or this
|
||||
local clickable = C.nameplates["clickthrough"] ~= "1" and true or false
|
||||
|
||||
-- disable all click events
|
||||
if not clickable then
|
||||
this:EnableMouse(false)
|
||||
this.nameplate:EnableMouse(false)
|
||||
else
|
||||
plate:EnableMouse(clickable)
|
||||
end
|
||||
|
||||
if C.nameplates["overlap"] == "1" then
|
||||
if this:GetWidth() > 1 then
|
||||
-- set parent to 1 pixel to have them overlap each other
|
||||
this:SetWidth(1)
|
||||
this:SetHeight(1)
|
||||
end
|
||||
else
|
||||
if not this.nameplate.dwidth then
|
||||
-- cache initial sizing value for comparison
|
||||
this.nameplate.dwidth = floor(this.nameplate:GetWidth() * UIParent:GetScale())
|
||||
end
|
||||
|
||||
if floor(this:GetWidth()) ~= this.nameplate.dwidth then
|
||||
-- align parent plate to the actual size
|
||||
this:SetWidth(this.nameplate:GetWidth() * UIParent:GetScale())
|
||||
this:SetHeight(this.nameplate:GetHeight() * UIParent:GetScale())
|
||||
end
|
||||
end
|
||||
|
||||
-- disable click events while spell is targeting
|
||||
local mouseEnabled = this.nameplate:IsMouseEnabled()
|
||||
if C.nameplates["clickthrough"] == "0" and C.nameplates["overlap"] == "1" and SpellIsTargeting() == mouseEnabled then
|
||||
this.nameplate:EnableMouse(not mouseEnabled)
|
||||
end
|
||||
|
||||
hookOnUpdate(self)
|
||||
end
|
||||
|
||||
-- enable mouselook on rightbutton down
|
||||
nameplates.mouselook = CreateFrame("Frame", nil, UIParent)
|
||||
nameplates.mouselook.time = nil
|
||||
|
||||
+13
-291
@@ -54,7 +54,15 @@ pfUI:RegisterModule("nampower", "vanilla", function ()
|
||||
|
||||
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
|
||||
|
||||
@@ -76,189 +84,7 @@ pfUI:RegisterModule("nampower", "vanilla", function ()
|
||||
end)
|
||||
end
|
||||
|
||||
-- Enhanced Debuff Tracking using Nampower events
|
||||
-- DEBUFF_ADDED_OTHER/DEBUFF_REMOVED_OTHER provide accurate debuff tracking with spellId
|
||||
if libdebuff then
|
||||
-- Storage for GUID-based debuff tracking
|
||||
pfUI.nampower_debuffs = pfUI.nampower_debuffs or {}
|
||||
local debuffdb = pfUI.nampower_debuffs
|
||||
|
||||
-- Get player GUID for tracking own debuffs
|
||||
local playerGuid
|
||||
|
||||
local debuffTracker = CreateFrame("Frame")
|
||||
debuffTracker:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
debuffTracker:RegisterEvent("DEBUFF_ADDED_OTHER")
|
||||
debuffTracker:RegisterEvent("DEBUFF_REMOVED_OTHER")
|
||||
debuffTracker:RegisterEvent("DEBUFF_ADDED_SELF")
|
||||
debuffTracker:RegisterEvent("DEBUFF_REMOVED_SELF")
|
||||
|
||||
debuffTracker:SetScript("OnEvent", function()
|
||||
if event == "PLAYER_ENTERING_WORLD" then
|
||||
-- Cache player GUID
|
||||
if UnitExists then
|
||||
local _, guid = UnitExists("player")
|
||||
playerGuid = guid
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- DEBUFF events: arg1=guid, arg2=slot, arg3=spellId, arg4=stackCount, arg5=auraLevel
|
||||
local guid = arg1
|
||||
local slot = arg2
|
||||
local spellId = arg3
|
||||
local stackCount = arg4
|
||||
local auraLevel = arg5
|
||||
|
||||
if not guid or not spellId then return end
|
||||
|
||||
if event == "DEBUFF_ADDED_OTHER" or event == "DEBUFF_ADDED_SELF" then
|
||||
-- Initialize storage for this GUID
|
||||
if not debuffdb[guid] then debuffdb[guid] = {} end
|
||||
|
||||
-- Get spell info
|
||||
local spellName, spellRank, texture
|
||||
if SpellInfo then
|
||||
spellName, spellRank, texture = SpellInfo(spellId)
|
||||
end
|
||||
if not spellName then
|
||||
spellName, spellRank = SafeGetSpellNameAndRank(spellId)
|
||||
end
|
||||
|
||||
if spellName then
|
||||
-- Get duration from libdebuff's duration table or GetSpellRec
|
||||
local duration = 0
|
||||
if libdebuff.GetDuration then
|
||||
duration = libdebuff:GetDuration(spellName, spellRank)
|
||||
end
|
||||
|
||||
-- Try GetSpellRec for duration if libdebuff doesn't have it
|
||||
if duration == 0 and GetSpellRec and spellId then
|
||||
local success, spellRec = pcall(GetSpellRec, spellId)
|
||||
if success and spellRec and spellRec.durationIndex and spellRec.durationIndex > 0 then
|
||||
-- Duration index maps to spell duration - common values:
|
||||
-- This is a rough approximation since we don't have the duration table
|
||||
duration = 30 -- Default fallback
|
||||
end
|
||||
end
|
||||
|
||||
-- Store debuff data
|
||||
debuffdb[guid][spellId] = {
|
||||
spellId = spellId,
|
||||
name = spellName,
|
||||
rank = spellRank,
|
||||
texture = texture,
|
||||
stacks = stackCount or 1,
|
||||
start = GetTime(),
|
||||
duration = duration,
|
||||
slot = slot,
|
||||
auraLevel = auraLevel,
|
||||
caster = (event == "DEBUFF_ADDED_SELF" or guid == playerGuid) and "player" or nil
|
||||
}
|
||||
|
||||
-- Also update libdebuff's internal tracking if we have unit info
|
||||
local unitName = UnitName and guid and UnitName(guid)
|
||||
local unitLevel = UnitLevel and guid and UnitLevel(guid)
|
||||
if unitName and duration > 0 then
|
||||
libdebuff:AddEffect(unitName, unitLevel or 0, spellName, duration, "player")
|
||||
end
|
||||
end
|
||||
|
||||
elseif event == "DEBUFF_REMOVED_OTHER" or event == "DEBUFF_REMOVED_SELF" then
|
||||
-- Remove debuff from tracking
|
||||
if debuffdb[guid] and debuffdb[guid][spellId] then
|
||||
debuffdb[guid][spellId] = nil
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Enhanced UnitDebuff function that uses Nampower data
|
||||
-- This provides more accurate debuff information when available
|
||||
local originalUnitDebuff = libdebuff.UnitDebuff
|
||||
function libdebuff:UnitDebuffNampower(unit, id)
|
||||
-- First try the original method
|
||||
local effect, rank, texture, stacks, dtype, duration, timeleft, caster = originalUnitDebuff(self, unit, id)
|
||||
|
||||
-- If we have Nampower data for this unit, try to enhance it
|
||||
if not UnitExists then return effect, rank, texture, stacks, dtype, duration, timeleft, caster end
|
||||
|
||||
local exists, guid = UnitExists(unit)
|
||||
if not exists or not guid or not debuffdb[guid] then
|
||||
return effect, rank, texture, stacks, dtype, duration, timeleft, caster
|
||||
end
|
||||
|
||||
-- Find the debuff by slot
|
||||
for spellId, data in pairs(debuffdb[guid]) do
|
||||
if data.slot == id then
|
||||
-- Use Nampower data for more accurate timing
|
||||
if data.duration and data.duration > 0 and data.start then
|
||||
duration = data.duration
|
||||
timeleft = data.duration + data.start - GetTime()
|
||||
if timeleft < 0 then timeleft = 0 end
|
||||
caster = data.caster
|
||||
stacks = data.stacks or stacks
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return effect, rank, texture, stacks, dtype, duration, timeleft, caster
|
||||
end
|
||||
|
||||
-- Expose enhanced function
|
||||
pfUI.api.libdebuff_nampower = libdebuff.UnitDebuffNampower
|
||||
end
|
||||
|
||||
-- Enhanced buff tracking using BUFF events
|
||||
if C.unitframes.nampower_buffs == "1" then
|
||||
pfUI.nampower_buffs = pfUI.nampower_buffs or {}
|
||||
local buffdb = pfUI.nampower_buffs
|
||||
|
||||
local buffTracker = CreateFrame("Frame")
|
||||
buffTracker:RegisterEvent("BUFF_ADDED_OTHER")
|
||||
buffTracker:RegisterEvent("BUFF_REMOVED_OTHER")
|
||||
buffTracker:RegisterEvent("BUFF_ADDED_SELF")
|
||||
buffTracker:RegisterEvent("BUFF_REMOVED_SELF")
|
||||
|
||||
buffTracker:SetScript("OnEvent", function()
|
||||
local guid = arg1
|
||||
local slot = arg2
|
||||
local spellId = arg3
|
||||
local stackCount = arg4
|
||||
local auraLevel = arg5
|
||||
|
||||
if not guid or not spellId then return end
|
||||
|
||||
if event == "BUFF_ADDED_OTHER" or event == "BUFF_ADDED_SELF" then
|
||||
if not buffdb[guid] then buffdb[guid] = {} end
|
||||
|
||||
local spellName, spellRank, texture
|
||||
if SpellInfo then
|
||||
spellName, spellRank, texture = SpellInfo(spellId)
|
||||
end
|
||||
if not spellName then
|
||||
spellName, spellRank = SafeGetSpellNameAndRank(spellId)
|
||||
end
|
||||
|
||||
if spellName then
|
||||
buffdb[guid][spellId] = {
|
||||
spellId = spellId,
|
||||
name = spellName,
|
||||
rank = spellRank,
|
||||
texture = texture,
|
||||
stacks = stackCount or 1,
|
||||
start = GetTime(),
|
||||
slot = slot,
|
||||
auraLevel = auraLevel
|
||||
}
|
||||
end
|
||||
elseif event == "BUFF_REMOVED_OTHER" or event == "BUFF_REMOVED_SELF" then
|
||||
if buffdb[guid] and buffdb[guid][spellId] then
|
||||
buffdb[guid][spellId] = nil
|
||||
end
|
||||
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
|
||||
@@ -429,22 +255,8 @@ pfUI:RegisterModule("nampower", "vanilla", function ()
|
||||
end
|
||||
end
|
||||
|
||||
-- UNIT_DIED event handling
|
||||
-- Can be used to clear tracking data or trigger effects on unit death
|
||||
local deathTracker = CreateFrame("Frame")
|
||||
deathTracker:RegisterEvent("UNIT_DIED")
|
||||
deathTracker:SetScript("OnEvent", function()
|
||||
local guid = arg1
|
||||
if not guid then return end
|
||||
|
||||
-- Clean up debuff tracking for dead units
|
||||
if pfUI.nampower_debuffs and pfUI.nampower_debuffs[guid] then
|
||||
pfUI.nampower_debuffs[guid] = nil
|
||||
end
|
||||
if pfUI.nampower_buffs and pfUI.nampower_buffs[guid] then
|
||||
pfUI.nampower_buffs[guid] = nil
|
||||
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
|
||||
@@ -667,96 +479,6 @@ pfUI:RegisterModule("nampower", "vanilla", function ()
|
||||
end
|
||||
end
|
||||
|
||||
-- Enhanced Heal Prediction with AURA_CAST events
|
||||
-- This helps libpredict detect HoT applications more accurately
|
||||
if libpredict then
|
||||
local auraCastFrame = CreateFrame("Frame")
|
||||
auraCastFrame:RegisterEvent("AURA_CAST_ON_SELF")
|
||||
auraCastFrame:RegisterEvent("AURA_CAST_ON_OTHER")
|
||||
|
||||
auraCastFrame:SetScript("OnEvent", function()
|
||||
local casterGuid = arg1
|
||||
local targetGuid = arg2
|
||||
local spellId = arg3
|
||||
|
||||
if not spellId or not targetGuid then return end
|
||||
|
||||
-- Get spell name
|
||||
local spellName
|
||||
if SpellInfo then
|
||||
spellName = SpellInfo(spellId)
|
||||
end
|
||||
if not spellName then
|
||||
spellName = SafeGetSpellNameAndRank(spellId)
|
||||
end
|
||||
|
||||
if not spellName then return end
|
||||
|
||||
-- Check if this is a HoT spell we care about
|
||||
local hotSpells = {
|
||||
["Rejuvenation"] = true,
|
||||
["Renew"] = true,
|
||||
["Regrowth"] = true,
|
||||
["Verjüngung"] = true, -- German
|
||||
["Erneuerung"] = true,
|
||||
["Nachwachsen"] = true,
|
||||
}
|
||||
|
||||
if hotSpells[spellName] then
|
||||
-- Signal to libpredict that a HoT was applied
|
||||
-- This can be used to update heal predictions
|
||||
if pfUI.api.libpredict and pfUI.api.libpredict.OnHotApplied then
|
||||
pfUI.api.libpredict:OnHotApplied(targetGuid, spellName, spellId)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Swing Timer Integration
|
||||
-- Track auto-attack swing timers for melee classes
|
||||
local swingFrame = CreateFrame("Frame")
|
||||
swingFrame.mainHand = { start = 0, speed = 0 }
|
||||
swingFrame.offHand = { start = 0, speed = 0 }
|
||||
swingFrame.ranged = { start = 0, speed = 0 }
|
||||
|
||||
swingFrame:RegisterEvent("PLAYER_ENTER_COMBAT")
|
||||
swingFrame:RegisterEvent("PLAYER_LEAVE_COMBAT")
|
||||
swingFrame:RegisterEvent("CHAT_MSG_COMBAT_SELF_HITS")
|
||||
swingFrame:RegisterEvent("CHAT_MSG_COMBAT_SELF_MISSES")
|
||||
|
||||
swingFrame:SetScript("OnEvent", function()
|
||||
if event == "PLAYER_ENTER_COMBAT" then
|
||||
local mainSpeed, offSpeed = UnitAttackSpeed("player")
|
||||
this.mainHand.speed = mainSpeed or 2
|
||||
this.offHand.speed = offSpeed or 0
|
||||
this.mainHand.start = GetTime()
|
||||
if this.offHand.speed > 0 then
|
||||
this.offHand.start = GetTime()
|
||||
end
|
||||
elseif event == "CHAT_MSG_COMBAT_SELF_HITS" or event == "CHAT_MSG_COMBAT_SELF_MISSES" then
|
||||
-- Reset swing timer on hit/miss
|
||||
local mainSpeed, offSpeed = UnitAttackSpeed("player")
|
||||
this.mainHand.speed = mainSpeed or 2
|
||||
this.mainHand.start = GetTime()
|
||||
end
|
||||
end)
|
||||
|
||||
pfUI.api.GetSwingTimers = function()
|
||||
local now = GetTime()
|
||||
local mainRemaining = swingFrame.mainHand.speed - (now - swingFrame.mainHand.start)
|
||||
local offRemaining = swingFrame.offHand.speed > 0 and (swingFrame.offHand.speed - (now - swingFrame.offHand.start)) or 0
|
||||
|
||||
return {
|
||||
mainHand = {
|
||||
remaining = math.max(0, mainRemaining),
|
||||
speed = swingFrame.mainHand.speed,
|
||||
progress = swingFrame.mainHand.speed > 0 and (1 - math.max(0, mainRemaining) / swingFrame.mainHand.speed) or 0,
|
||||
},
|
||||
offHand = {
|
||||
remaining = math.max(0, offRemaining),
|
||||
speed = swingFrame.offHand.speed,
|
||||
progress = swingFrame.offHand.speed > 0 and (1 - math.max(0, offRemaining) / swingFrame.offHand.speed) or 0,
|
||||
},
|
||||
}
|
||||
end
|
||||
-- NOTE: HoT Detection (AURA_CAST events) removed - OnHotApplied callback was never implemented in libpredict
|
||||
-- NOTE: Swing Timer removed - GetSwingTimers() was never called anywhere
|
||||
end)
|
||||
+2
-2
@@ -15,8 +15,8 @@ pfUI:RegisterModule("player", "vanilla:tbc", function ()
|
||||
if pfUI.uf.player:GetScript("OnUpdate") then
|
||||
local originalOnUpdate = pfUI.uf.player:GetScript("OnUpdate")
|
||||
pfUI.uf.player:SetScript("OnUpdate", function()
|
||||
if (this.tick or 0) > GetTime() then return end
|
||||
this.tick = GetTime() + 0.1
|
||||
if (this.throttleTick or 0) > GetTime() then return end
|
||||
this.throttleTick = GetTime() + 0.1
|
||||
originalOnUpdate()
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -113,6 +113,27 @@ 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()
|
||||
|
||||
+320
-67
@@ -161,15 +161,42 @@ pfUI:RegisterModule("superwow", "vanilla", function ()
|
||||
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
|
||||
-- Uses Nampower's GetUnitField to get base mana when in shapeshift form
|
||||
local hasNampower = (GetUnitField ~= nil)
|
||||
|
||||
-- Add support for player secondary power bar (shows base mana when in shapeshift form)
|
||||
-- Works with SuperWoW OR Nampower - both extend UnitMana() to return base mana as second value
|
||||
-- Only for Druids (only class that can shapeshift in Vanilla)
|
||||
-- Controlled by "Show Druid Mana Bar" setting
|
||||
local _, playerClass = UnitClass("player")
|
||||
if hasNampower and pfUI.uf and pfUI.uf.player and playerClass == "DRUID" and pfUI_config.unitframes.druidmanabar == "1" then
|
||||
local rawborder, default_border = GetBorderSize("unitframes")
|
||||
local config = pfUI.uf.player.config
|
||||
|
||||
-- Create secondary mana bar below the power bar
|
||||
local playerMana = CreateFrame("StatusBar", "pfPlayerSecondaryMana", pfUI.uf.player)
|
||||
playerMana:SetFrameStrata(pfUI.uf.player:GetFrameStrata())
|
||||
playerMana:SetFrameLevel(pfUI.uf.player:GetFrameLevel() + 5)
|
||||
playerMana:SetStatusBarTexture(pfUI.media[config.pbartexture])
|
||||
|
||||
-- Mana color
|
||||
local manacolor = config.defcolor == "0" and config.manacolor or C.unitframes.manacolor
|
||||
local r, g, b, a = pfUI.api.strsplit(",", manacolor)
|
||||
playerMana:SetStatusBarColor(r, g, b, a)
|
||||
|
||||
-- Use SAME size as normal power bar (pwidth/pheight from config)
|
||||
local width = config.pwidth ~= "-1" and config.pwidth or config.width
|
||||
local height = config.pheight
|
||||
playerMana:SetWidth(width)
|
||||
playerMana:SetHeight(height)
|
||||
playerMana:SetPoint("TOPLEFT", pfUI.uf.player.power, "BOTTOMLEFT", 0, -2*default_border - (config.pspace or 0))
|
||||
playerMana:SetPoint("TOPRIGHT", pfUI.uf.player.power, "BOTTOMRIGHT", 0, -2*default_border - (config.pspace or 0))
|
||||
playerMana:Hide()
|
||||
|
||||
CreateBackdrop(playerMana)
|
||||
CreateBackdropShadow(playerMana)
|
||||
|
||||
-- Text overlay - same font settings 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
|
||||
@@ -180,75 +207,285 @@ pfUI:RegisterModule("superwow", "vanilla", function ()
|
||||
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()
|
||||
playerMana.text = playerMana:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
||||
playerMana.text:SetFontObject(GameFontWhite)
|
||||
playerMana.text:SetFont(fontname, fontsize, fontstyle)
|
||||
playerMana.text:SetPoint("CENTER", playerMana, "CENTER", 0, 0)
|
||||
|
||||
-- Set text color like normal power bar (mana = type 0)
|
||||
local tr, tg, tb = 1, 1, 1
|
||||
if config["powercolor"] == "1" then
|
||||
tr = ManaBarColor[0].r
|
||||
tg = ManaBarColor[0].g
|
||||
tb = ManaBarColor[0].b
|
||||
end
|
||||
if C.unitframes.pastel == "1" then
|
||||
tr, tg, tb = (tr + .75) * .5, (tg + .75) * .5, (tb + .75) * .5
|
||||
end
|
||||
playerMana.text:SetTextColor(tr, tg, tb, 1)
|
||||
|
||||
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()
|
||||
-- Update function
|
||||
local function UpdatePlayerSecondaryMana()
|
||||
local powerType = UnitPowerType("player")
|
||||
|
||||
-- Only show when NOT using mana (i.e., in Bear/Cat form)
|
||||
if powerType == 0 then
|
||||
playerMana:Hide()
|
||||
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))
|
||||
-- Get base mana using Nampower's GetUnitField
|
||||
local baseMana, baseMaxMana
|
||||
|
||||
if GetUnitField then
|
||||
local _, guid = UnitExists("player")
|
||||
if guid then
|
||||
baseMana = GetUnitField(guid, "power1")
|
||||
baseMaxMana = GetUnitField(guid, "maxPower1")
|
||||
end
|
||||
end
|
||||
|
||||
-- Round down power values (Nampower can return decimals, especially for rage)
|
||||
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
|
||||
playerMana:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
-- Update bar
|
||||
playerMana:SetMinMaxValues(0, baseMaxMana)
|
||||
playerMana:SetValue(baseMana)
|
||||
|
||||
-- Update text based on power text config (uses same setting as normal power bar)
|
||||
-- Check txtpowercenter first (centered text), then txtpowerright, then txtpowerleft
|
||||
local textConfig = config.txtpowercenter or config.txtpowerright or config.txtpowerleft
|
||||
|
||||
if not textConfig or textConfig == "" or textConfig == "none" then
|
||||
-- No text configured - hide text
|
||||
playerMana.text:SetText("")
|
||||
elseif textConfig == "power" then
|
||||
playerMana.text:SetText(Abbreviate(baseMana))
|
||||
elseif textConfig == "powermax" then
|
||||
playerMana.text:SetText(Abbreviate(baseMaxMana))
|
||||
elseif textConfig == "powerperc" then
|
||||
local perc = math.ceil(baseMana / baseMaxMana * 100)
|
||||
playerMana.text:SetText(perc)
|
||||
elseif textConfig == "powermiss" then
|
||||
local miss = math.ceil(baseMana - baseMaxMana)
|
||||
playerMana.text:SetText(miss == 0 and "0" or Abbreviate(miss))
|
||||
elseif textConfig == "powerdyn" then
|
||||
local perc = math.ceil(baseMana / baseMaxMana * 100)
|
||||
if perc == 100 then
|
||||
playerMana.text:SetText(Abbreviate(baseMana))
|
||||
else
|
||||
playerMana.text:SetText(string.format("%s - %s%%", Abbreviate(baseMana), perc))
|
||||
end
|
||||
elseif textConfig == "powerminmax" then
|
||||
playerMana.text:SetText(string.format("%s/%s", Abbreviate(baseMana), Abbreviate(baseMaxMana)))
|
||||
else
|
||||
-- Default: show dynamic (value + percentage if not full)
|
||||
local perc = math.ceil(baseMana / baseMaxMana * 100)
|
||||
if perc == 100 then
|
||||
playerMana.text:SetText(Abbreviate(baseMana))
|
||||
else
|
||||
playerMana.text:SetText(string.format("%s - %s%%", Abbreviate(baseMana), perc))
|
||||
end
|
||||
end
|
||||
|
||||
playerMana:Show()
|
||||
end
|
||||
|
||||
-- Register events
|
||||
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()
|
||||
-- Handle shutdown to prevent crash 132
|
||||
if event == "PLAYER_LOGOUT" then
|
||||
this:UnregisterAllEvents()
|
||||
this:SetScript("OnEvent", nil)
|
||||
return
|
||||
end
|
||||
if arg1 == nil or arg1 == "player" then
|
||||
UpdatePlayerSecondaryMana()
|
||||
end
|
||||
this:SetMinMaxValues(0, max)
|
||||
this:SetValue(mana)
|
||||
this:Show()
|
||||
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")
|
||||
-- Initial update
|
||||
UpdatePlayerSecondaryMana()
|
||||
|
||||
if config["powercolor"] == "1" then
|
||||
local r = ManaBarColor[0].r
|
||||
local g = ManaBarColor[0].g
|
||||
local b = ManaBarColor[0].b
|
||||
|
||||
if pfUI_config.unitframes.pastel == "1" then
|
||||
druidmana.text:SetTextColor((r+.75)*.5, (g+.75)*.5, (b+.75)*.5, 1)
|
||||
else
|
||||
druidmana.text:SetTextColor(r, g, b, a)
|
||||
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
|
||||
-- Store reference
|
||||
pfUI.uf.player.secondaryMana = playerMana
|
||||
end
|
||||
|
||||
-- Add support for target secondary power bar (shows base mana when target is in shapeshift form)
|
||||
-- Works with SuperWoW OR Nampower - both extend UnitMana() to return base mana as second value
|
||||
-- Controlled by "Show Druid Mana Bar" setting - available for ALL classes
|
||||
if hasNampower and pfUI.uf and pfUI.uf.target and pfUI_config.unitframes.druidmanabar == "1" then
|
||||
local rawborder, default_border = GetBorderSize("unitframes")
|
||||
local config = pfUI.uf.target.config
|
||||
|
||||
-- Create secondary mana bar below the power bar
|
||||
local targetMana = CreateFrame("StatusBar", "pfTargetSecondaryMana", pfUI.uf.target)
|
||||
targetMana:SetFrameStrata(pfUI.uf.target:GetFrameStrata())
|
||||
targetMana:SetFrameLevel(pfUI.uf.target:GetFrameLevel() + 5)
|
||||
targetMana:SetStatusBarTexture(pfUI.media[config.pbartexture])
|
||||
|
||||
-- Mana color
|
||||
local manacolor = config.defcolor == "0" and config.manacolor or C.unitframes.manacolor
|
||||
local r, g, b, a = pfUI.api.strsplit(",", manacolor)
|
||||
targetMana:SetStatusBarColor(r, g, b, a)
|
||||
|
||||
-- Use SAME size as normal power bar (pwidth/pheight from config)
|
||||
local width = config.pwidth ~= "-1" and config.pwidth or config.width
|
||||
local height = config.pheight -- Same height as power bar!
|
||||
targetMana:SetWidth(width)
|
||||
targetMana:SetHeight(height)
|
||||
targetMana:SetPoint("TOPLEFT", pfUI.uf.target.power, "BOTTOMLEFT", 0, -2*default_border - (config.pspace or 0))
|
||||
targetMana:SetPoint("TOPRIGHT", pfUI.uf.target.power, "BOTTOMRIGHT", 0, -2*default_border - (config.pspace or 0))
|
||||
targetMana:Hide()
|
||||
|
||||
CreateBackdrop(targetMana)
|
||||
CreateBackdropShadow(targetMana)
|
||||
|
||||
-- Text overlay - same font settings 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 config.customfont == "1" then
|
||||
fontname = pfUI.media[config.customfont_name]
|
||||
fontsize = tonumber(config.customfont_size)
|
||||
fontstyle = config.customfont_style
|
||||
end
|
||||
|
||||
targetMana.text = targetMana:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
||||
targetMana.text:SetFontObject(GameFontWhite)
|
||||
targetMana.text:SetFont(fontname, fontsize, fontstyle)
|
||||
targetMana.text:SetPoint("CENTER", targetMana, "CENTER", 0, 0)
|
||||
|
||||
-- Set text color like normal power bar (mana = type 0)
|
||||
-- Uses powercolor setting and pastel effect
|
||||
local function UpdateTextColor()
|
||||
local tr, tg, tb = 1, 1, 1
|
||||
if config["powercolor"] == "1" then
|
||||
tr = ManaBarColor[0].r
|
||||
tg = ManaBarColor[0].g
|
||||
tb = ManaBarColor[0].b
|
||||
end
|
||||
if C.unitframes.pastel == "1" then
|
||||
tr, tg, tb = (tr + .75) * .5, (tg + .75) * .5, (tb + .75) * .5
|
||||
end
|
||||
targetMana.text:SetTextColor(tr, tg, tb, 1)
|
||||
end
|
||||
UpdateTextColor()
|
||||
|
||||
-- Update function
|
||||
local function UpdateTargetSecondaryMana()
|
||||
if not UnitExists("target") then
|
||||
targetMana:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
local powerType = UnitPowerType("target")
|
||||
|
||||
-- Only show if target is NOT using mana (i.e., in shapeshift form with energy/rage)
|
||||
if powerType == 0 then
|
||||
-- Target is using mana as primary power - no need for secondary bar
|
||||
targetMana:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
-- Get base mana using Nampower's GetUnitField
|
||||
local baseMana, baseMaxMana
|
||||
|
||||
if GetUnitField then
|
||||
local _, guid = UnitExists("target")
|
||||
if guid then
|
||||
baseMana = GetUnitField(guid, "power1")
|
||||
baseMaxMana = GetUnitField(guid, "maxPower1")
|
||||
end
|
||||
end
|
||||
|
||||
-- Round down power values (Nampower can return decimals, especially for rage)
|
||||
if baseMana then baseMana = math.floor(baseMana) end
|
||||
if baseMaxMana then baseMaxMana = math.floor(baseMaxMana) end
|
||||
|
||||
-- Check if we got valid mana values
|
||||
if type(baseMana) ~= "number" or type(baseMaxMana) ~= "number" or baseMaxMana == 0 then
|
||||
targetMana:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
-- Update bar
|
||||
targetMana:SetMinMaxValues(0, baseMaxMana)
|
||||
targetMana:SetValue(baseMana)
|
||||
|
||||
-- Update text based on power text config (uses same setting as normal power bar)
|
||||
local textConfig = config.txtpowercenter or config.txtpowerright or config.txtpowerleft
|
||||
|
||||
if not textConfig or textConfig == "" or textConfig == "none" then
|
||||
targetMana.text:SetText("")
|
||||
elseif textConfig == "power" then
|
||||
targetMana.text:SetText(Abbreviate(baseMana))
|
||||
elseif textConfig == "powermax" then
|
||||
targetMana.text:SetText(Abbreviate(baseMaxMana))
|
||||
elseif textConfig == "powerperc" then
|
||||
local perc = math.ceil(baseMana / baseMaxMana * 100)
|
||||
targetMana.text:SetText(perc)
|
||||
elseif textConfig == "powermiss" then
|
||||
local miss = math.ceil(baseMana - baseMaxMana)
|
||||
targetMana.text:SetText(miss == 0 and "0" or Abbreviate(miss))
|
||||
elseif textConfig == "powerdyn" then
|
||||
local perc = math.ceil(baseMana / baseMaxMana * 100)
|
||||
if perc == 100 then
|
||||
targetMana.text:SetText(Abbreviate(baseMana))
|
||||
else
|
||||
targetMana.text:SetText(string.format("%s - %s%%", Abbreviate(baseMana), perc))
|
||||
end
|
||||
elseif textConfig == "powerminmax" then
|
||||
targetMana.text:SetText(string.format("%s/%s", Abbreviate(baseMana), Abbreviate(baseMaxMana)))
|
||||
else
|
||||
local perc = math.ceil(baseMana / baseMaxMana * 100)
|
||||
if perc == 100 then
|
||||
targetMana.text:SetText(Abbreviate(baseMana))
|
||||
else
|
||||
targetMana.text:SetText(string.format("%s - %s%%", Abbreviate(baseMana), perc))
|
||||
end
|
||||
end
|
||||
|
||||
targetMana:Show()
|
||||
end
|
||||
|
||||
-- Register events
|
||||
targetMana:RegisterEvent("PLAYER_TARGET_CHANGED")
|
||||
targetMana:RegisterEvent("UNIT_MANA")
|
||||
targetMana:RegisterEvent("UNIT_MAXMANA")
|
||||
targetMana:RegisterEvent("UNIT_DISPLAYPOWER")
|
||||
targetMana:RegisterEvent("PLAYER_LOGOUT")
|
||||
targetMana:SetScript("OnEvent", function()
|
||||
-- Handle shutdown to prevent crash 132
|
||||
if event == "PLAYER_LOGOUT" then
|
||||
this:UnregisterAllEvents()
|
||||
this:SetScript("OnEvent", nil)
|
||||
return
|
||||
end
|
||||
if event == "PLAYER_TARGET_CHANGED" then
|
||||
UpdateTargetSecondaryMana()
|
||||
elseif arg1 == "target" then
|
||||
UpdateTargetSecondaryMana()
|
||||
end
|
||||
end)
|
||||
|
||||
-- Store reference
|
||||
pfUI.uf.target.secondaryMana = targetMana
|
||||
end
|
||||
|
||||
|
||||
-- Add support for guid based focus frame
|
||||
if SUPERWOW_VERSION and pfUI.uf and pfUI.uf.focus then
|
||||
local focus = function(unitstr)
|
||||
@@ -312,8 +549,16 @@ pfUI:RegisterModule("superwow", "vanilla", function ()
|
||||
trackFrame:RegisterEvent("PARTY_MEMBERS_CHANGED")
|
||||
trackFrame:RegisterEvent("RAID_ROSTER_UPDATE")
|
||||
trackFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
trackFrame:RegisterEvent("PLAYER_LOGOUT")
|
||||
|
||||
trackFrame:SetScript("OnEvent", function()
|
||||
-- Handle shutdown to prevent crash 132
|
||||
if event == "PLAYER_LOGOUT" then
|
||||
this:UnregisterAllEvents()
|
||||
this:SetScript("OnEvent", nil)
|
||||
return
|
||||
end
|
||||
|
||||
-- Track party members
|
||||
for i = 1, 4 do
|
||||
local unit = "party" .. i
|
||||
@@ -483,7 +728,15 @@ pfUI:RegisterModule("superwow", "vanilla", function ()
|
||||
|
||||
supercast:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
supercast:RegisterEvent("UNIT_CASTEVENT")
|
||||
supercast:RegisterEvent("PLAYER_LOGOUT")
|
||||
supercast:SetScript("OnEvent", function()
|
||||
-- Handle shutdown to prevent crash 132
|
||||
if event == "PLAYER_LOGOUT" then
|
||||
this:UnregisterAllEvents()
|
||||
this:SetScript("OnEvent", nil)
|
||||
return
|
||||
end
|
||||
|
||||
if event == "PLAYER_ENTERING_WORLD" then
|
||||
-- Cache player GUID
|
||||
if UnitExists then
|
||||
|
||||
@@ -90,7 +90,24 @@ pfUI:RegisterModule("unitxp", "vanilla", function ()
|
||||
-- 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)
|
||||
@@ -102,8 +119,16 @@ pfUI:RegisterModule("unitxp", "vanilla", function ()
|
||||
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)
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
## Interface: 20400
|
||||
## Title: |cff33ffccpf|cffffffffUI
|
||||
## Author: Shagu
|
||||
## Author: Shagu - modfied by me0wg4ming
|
||||
## Notes: A complete user interface replacement.
|
||||
## Notes-ruRU: Полная замена пользовательского интерфейса.
|
||||
## Version: 6.2.6
|
||||
## Version: 7.6.0 (experiment version)
|
||||
## SavedVariables: pfUI_profiles, pfUI_addon_profiles, pfUI_cache
|
||||
## SavedVariablesPerCharacter: pfUI_config, pfUI_init, pfUI_playerDB
|
||||
|
||||
|
||||
@@ -47,6 +47,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
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
## Interface: 11200
|
||||
## Title: |cff33ffccpf|cffffffffUI
|
||||
## Author: Shagu
|
||||
## Author: Shagu - modfied by me0wg4ming
|
||||
## Notes: A complete user interface replacement.
|
||||
## Notes-ruRU: Полная замена пользовательского интерфейса.
|
||||
## Version: 6.2.6
|
||||
## Version: 7.6.0 (experiment version)
|
||||
## SavedVariables: pfUI_profiles, pfUI_addon_profiles, pfUI_cache
|
||||
## SavedVariablesPerCharacter: pfUI_config, pfUI_init, pfUI_playerDB
|
||||
|
||||
|
||||
Reference in New Issue
Block a user