39 Commits

Author SHA1 Message Date
Brues 64e3f93042 Specifically disable pfUI mouseover hooks when unit attribute exists 2026-08-02 15:11:46 -05:00
Brues 0c86ed67e9 Revert "Drop redundant pfUI mouseover frame hooks; pfUI sets native mouseover"
This reverts commit c5bc69560e.
2026-08-02 15:07:35 -05:00
Brues 60c1b7f235 dont bother hooking into brues-code pfUI 2026-08-01 20:21:37 -05:00
Brues 8c10353567 removed useless GetSpellRecField checks 2026-07-30 20:41:23 -05:00
Brues fcd206756d Replace equipment cache with C_Item.IsEquippedItem
HasGearEquipped now wraps ClassicAPI's native slot-walk, which
short-circuits on first match. Removes BuildEquipmentCache, the
_equipped* cache tables/invalidation, and the InvalidateEquipmentCache
call from PLAYER_EQUIPMENT_CHANGED. [equipped] reads live engine state,
so no cache staleness surface remains; also gains item-link support.
2026-07-30 20:35:39 -05:00
Brues 8d86389a03 Remove dead IndexEquippedItems (no callers) 2026-07-30 20:28:39 -05:00
Brues 8c85ce572a Scope PLAYER_EQUIPMENT_CHANGED to the changed slot
Replace the full IndexItems + action-bar rebuild with a single-slot
IndexEquipSlot using the event's arg1 (slot) and arg2 (hasCurrent),
and drop the now-pointless throttle/deferral. Bag-side deltas remain
covered by BAG_UPDATE_DELAYED; [equipped] runs off its own cache.
2026-07-30 20:28:05 -05:00
Brues a2db3ebd42 Add indoors/outdoors conditionals via ClassicAPI 2026-07-30 19:59:50 -05:00
Brues 66d5f0c618 Switch to PLAYER_EQUIPMENT_CHANGED event
Replace UNIT_INVENTORY_CHANGED with PLAYER_EQUIPMENT_CHANGED in event registration and handler
2026-07-30 19:59:38 -05:00
Brues 7cf2ad95c3 Use BAG_UPDATE_DELAYED 2026-07-30 19:51:24 -05:00
Brues c5bc69560e Drop redundant pfUI mouseover frame hooks; pfUI sets native mouseover
pfUI.uf:EnableScripts binds pfUI.uf.OnEnter to every unitframe, which calls
Nampower's SetMouseoverUnit -- so the native "mouseover" token already resolves
to the hovered pfUI frame (player/target/party/raid/focus/pettarget/partytarget/
...). Every conditional consumer checks UnitExists("mouseover") before the
CleveRoids.mouseoverUnit fallback, so the 10 per-frame OnEnter/OnLeave hookers
here were pure duplication (they even chained pfUI's OnEnter).

Removed all of them. Kept RegisterRaidMarkScripts (raid-marker rows aren't
unitframes, so pfUI sets no mouseover for them -> still needed for mark1..mark8)
and HookPfCast (unrelated /pfcast conditional wrapper), plus the PfSet/PfClear/
resolve helpers the raid-mark path uses.
2026-07-30 00:28:00 -05:00
Brues ce10c9f456 Derive membership-only spell families by name, not hardcoded rank lists
Rip/Rake/Conflagrate/MoltenBlast/DarkHarvest were hardcoded {rankID=true} sets
used only for membership tests. Replaced with a metatable-backed RankSet(seedID):
t[spellID] name-matches the spellID against one seed rank via C_Spell.GetSpellName,
which resolves every rank (no enumeration) and TWoW's custom spells (confirmed
36916 -> 'Molten Blast' on the TWoW client). Read shape is unchanged, so all
consumers work untouched. Locale-safe (name derived from the seed ID). Custom
seeds absent on a stock client resolve to nil and simply never match -- correct,
those spells can't be cast there.

Left hardcoded: the pairs()-iterated sets (FerociousBite/FlameShock/Immolate),
BleedSpellIDs (Pounce cast/bleed name collision), ComboScalingSpellsByID (carries
base/increment), PounceToBleedMapping (trigger relationship), MobsThatBleed (GUIDs).
2026-07-28 12:21:38 -05:00
Brues 08dd3ba4dd Remove dead code from ComboPointTracker
PounceBleedSpellIDs, GetLastComboPointsForSpell, and GetLastDurationForSpell
were each definition-only with zero references anywhere in the addon. The
Pounce bleed IDs are still covered by BleedSpellIDs (detection) and
PounceToBleedMapping (cast->trigger).
2026-07-28 12:10:54 -05:00
Brues b1fb2af861 remove fallbacks 2026-07-28 10:39:41 -05:00
Brues 7382e8a55f Unify dispel scan on GetAuraDataByIndex with a filter
scanDispel now calls C_UnitAuras.GetAuraDataByIndex(unit, i, filter) instead of
selecting GetBuffDataByIndex/GetDebuffDataByIndex -- the two are documented as
GetAuraDataByIndex with a HELPFUL/HARMFUL filter, and the index runs within the
filtered range identically. Drops the helpful-and-A-or-B function pick.
2026-07-28 09:55:05 -05:00
Brues 37434911e4 Resolve tracked aura texture/stacks via C_UnitAuras, not slot scans
FindPlayerDebuff and FindPlayerBuff hand-scanned debuff/buff slots for a
spellID to pull icon + stacks. Both now use ClassicAPI.GetUnitAuraBySpellID
(AuraData.icon / .applications) -- FindPlayerDebuff unfiltered (walks both
ranges, was debuff-then-buff), FindPlayerBuff HELPFUL-filtered (buff-only).
Search order is immaterial since a spellID is only ever a buff or a debuff.
2026-07-28 09:20:23 -05:00
Brues da8ea22998 Match buff-based immunity via C_UnitAuras.GetAuraDataBySpellName
Replaces the 32-slot UnitBuff + C_Spell.GetSpellName name-compare loop with a
single ClassicAPI.GetAuraDataBySpellName(unit, name, "HELPFUL") lookup.
Locale-identical to the old compare (both use the localized Spell.dbc name).
Wrapper added next to GetUnitAuraBySpellID.
2026-07-28 09:16:33 -05:00
Brues d82eeabf22 Detect Banish via C_UnitAuras.GetUnitAuraBySpellID, not slot scan
The Banish immunity check hand-scanned 16 debuff slots plus 32 buff slots
(NPC overflow) for spellID 710/18647. ClassicAPI.GetUnitAuraBySpellID walks
the whole aura array in one call per rank, so it finds the debuff wherever it
lands -- including an NPC's overflow buff slots -- dropping the two loops and
the UnitIsPlayer gate. Wrapper added alongside UnitHasDispelType.
2026-07-28 09:12:29 -05:00
Brues dd61767eb9 Add [locked]/[nolocked] school-interrupt conditional via C_LossOfControl
Detects a spell-school interrupt lockout (Counterspell/Kick/Pummel/Earth
Shock) on the player -- the server-side lockout that no debuff scan can see,
since it's a SMSG_SPELL_COOLDOWN packet, not an aura. ClassicAPI's
C_LossOfControl aggregates it as SCHOOL_INTERRUPT with a lockoutSchool mask.

ClassicAPI.GetSchoolLockout() returns the locked school mask; ValidateSchoolLocked
tests a school name against it (bitmask check without the 5.1-only % operator).
[locked] = any school kicked, [locked:frost] = that school, [locked:fire/frost]
= either; [nolocked:...] negates with AND (neither). Registered in
BOOLEAN_CONDITIONALS for the bare form; LOSS_OF_CONTROL_ADDED/UPDATE wired to
refresh the icon. Full silences remain [mycc:silence].
2026-07-28 08:57:37 -05:00
Brues 2e8ed33a6a focus/focustarget is provided by classicapi now 2026-07-28 00:02:45 -05:00
Brues 403567b381 Scan nameplateN unit tokens for /target candidates, not SuperWoW WorldFrame
The /target candidate search's last SuperWoW-gated block walked WorldFrame
children and scraped GUIDs via pcall(frame.GetName, frame, 1). ClassicAPI now
provides modern nameplateN unit tokens that work with UnitExists/UnitCanAttack/
TargetUnit directly, so it collapses to iterating nameplate1..40 -- no SuperWoW
dependency, no WorldFrame hackery, and it also picks up default vanilla
nameplates. Scans the full range since assigned slots can be sparse.
2026-07-28 00:00:51 -05:00
Brues 7ecb66a8dc Add left/right modifier variants to kmods via ClassicAPI
kmods gains lctrl/rctrl, lalt/ralt, lshift/rshift (ClassicAPI's side-specific
IsLeft*/IsRight* key checks), so [mod:lshift]/[mod:ralt] and castsequence
reset=lctrl work. mod/nomod now use ClassicAPI's IsModifierKeyDown instead of
OR-ing the three individual checks.
2026-07-27 22:46:18 -05:00
Brues 494aced5bd Resolve [mhimbue:Name] via enchant DBC, not tooltip scan
CheckWeaponImbueByName now resolves the applied temp-enchant ID to its
localized name via ClassicAPI (C_Item.GetEnchantInfo) and matches that
directly -- exact and locale-clean, replacing the green-text tooltip
heuristics for named enchants (poisons, oils, Mongoose, Windfury). Only
nameless enchants (sharpening/weightstones, no DBC name) fall through to
the tooltip scan, so no regression.
2026-07-27 22:44:13 -05:00
Brues 4f76255f73 Mark GetWeaponEnchantInfo TODO item done 2026-07-27 22:42:47 -05:00
Brues fd8050aee1 Add [mhenchant]/[ohenchant] weapon temp-enchant conditionals
Uses ClassicAPI's modern C_Item.GetWeaponEnchantInfo (12-tuple with
enchant IDs) plus C_Item.GetEnchantInfo (ID -> localized name) to match
WHICH temporary weapon enchant is applied -- by SpellItemEnchantment ID or
name -- not just that one exists. Locale-proof and exact, unlike
[mhimbue:Name]'s green-text tooltip scan.

ClassicAPI.lua gains GetWeaponEnchant(slot) (centralizes the tuple
indexing, vanilla-global fallback) and GetEnchantName(id). ValidateWeaponImbue
now reads through GetWeaponEnchant. New keywords mhenchant/nomhenchant/
ohenchant/noohenchant support bare (any enchant), single ID/name, and
OR-lists ([mhenchant:2823/Deadly_Poison]); registered in BOOLEAN_CONDITIONALS
for the bare form and auto-added to VALID_CONDITIONALS via Keywords.
2026-07-27 22:42:09 -05:00
Brues 0e07eafbf9 Add [equipset]/[noequipset] conditionals for equipment sets
Checks whether a saved ClassicAPI equipment set is currently equipped, via
C_EquipmentSet.GetEquipmentSetInfo's isEquipped flag (wrapped as
ClassicAPI.IsEquipmentSetEquipped). Named [equipset] rather than [set] to avoid
colliding with the existing tier/item-set piece-count conditional. Supports
OR-lists ([equipset:Raid/Farm]) and negation ([noequipset:PvP]). Pairs with the
conditional /equipset command: swap with one, branch on the other.
2026-07-27 22:32:01 -05:00
Brues 6aaa8a409b Add conditional support to /equipset via ClassicAPI equipment sets
ClassicAPI registers a bare /equipset that resolves a name and calls
C_EquipmentSet.UseEquipmentSet with no conditional layer. DoEquipSet routes
it through DoWithConditionals like the other /equip* commands, so it gains
[combat]/[mod]/@unit/etc. and ';'-separated fallthrough (first set whose
conditionals pass wins). Console.lua reclaims the EQUIP_SET handler (keeping
ClassicAPI's localized aliases) and falls back to registering /equipset itself
if the API command isn't present.
2026-07-27 22:28:09 -05:00
Brues 2cf4886b01 Add [mana]/[rage]/[energy] type-specific power conditionals
Read a SPECIFIC power slot (Enum.PowerType) as a percentage via ClassicAPI
UnitPower/UnitPowerMax, rather than the unit's primary power. Adds mana/
mymana, rage/myrage, energy/myenergy (no-prefix = target, @unit-overridable;
my = player), each supporting operators and multi-comparison like [power]/
[mypower]. Works cross-form (druid mana in Cat) and cross-unit -- the
standout being [@target,mana:<15] to catch a caster near OOM. A unit with no
such pool (max <= 0) fails rather than reading 0%. Registered in the macro
error checker (needsArgs + operator hint); VALID_CONDITIONALS auto-populates
from Keywords.
2026-07-27 20:10:20 -05:00
Brues 77f985a927 Read HP deficit via ClassicAPI UnitHealthMissing
ValidateHpLost now gets the health deficit in one call through
CleveRoids.ClassicAPI.UnitHealthMissing, mirroring how ValidatePowerLost
uses UnitPowerMissing, instead of hand-computing max - current via
NampowerAPI.
2026-07-27 19:48:36 -05:00
Brues 7c71f8fc26 Use ClassicAPI UnitPower* for power reads; drop GetUnitField/SuperWoW paths
Add ClassicAPI UnitPower/UnitPowerMax/UnitPowerMissing/UnitPowerType wrappers
and route the power conditionals through them:
- ValidatePower/ValidateRawPower/GetCachedPlayerPower(Percent) use UnitPower /
  UnitPowerMax (omitted type = primary power, matching the old UnitMana path).
- ValidatePowerLost uses UnitPowerMissing (one call vs max - current).
- powertype/nopowertype use ClassicAPI.UnitPowerType.

Replace the SuperWoW "2nd return of UnitMana = caster mana" druid hack in
ValidateDruidRawMana and the #showtooltip OOM check with UnitPower(unit, 0),
which reads the mana slot directly and survives shapeshift. Verified in Cat
Form: UnitPower('player', 0) returns caster mana while UnitPower('player', 3)
returns energy.

Remove the now-dead Nampower GetUnitField-based GetUnitPower/GetUnitMaxPower
wrappers and POWER_FIELDS tables (no caller passed a powerType, so that path
never ran).
2026-07-27 19:27:25 -05:00
Brues efc98bcd6a Use ClassicAPI GetSpellBonusDamage/Healing for spell-power conditionals
Migrate the healing/*_power/spell_power conditionals off Nampower's
GetSpellPower to ClassicAPI GetSpellBonusDamage(school) / GetSpellBonusHealing.
Both read the same client field, so the per-school damage values are identical
while dropping the Nampower v2.31 gate.

Fixes healing/healingpower: GetSpellPower returns no healing value, so they were
returning the 2nd school (holy spell damage) as a stand-in for +healing.
GetSpellBonusHealing returns the real (derived) +healing.

Prune the now-unused Nampower GetSpellPower wrapper, feature-table entry, and
feature flag.
2026-07-27 18:57:58 -05:00
Brues 41d9283117 Use IsInGroup/IsInRaid for group checks
Replace numeric GetNumPartyMembers/GetNumRaidMembers checks with IsInGroup() and IsInRaid() in Conditionals.lua for the group and nogroup conditionals.
2026-07-27 18:45:02 -05:00
Brues 9bebbbb836 Fix addon initialization order with guard checks
Refactor mouseover extensions to split initialization into two phases: OnLoad (empty) and OnAddOnLoad (actual setup). This ensures hooks are only registered when the target addon's globals are defined. Add guard clauses to check for required functions before hooking. Also remove Cursive's premature initialization attempt.
2026-07-27 14:32:12 -05:00
Brues 586c51b53a Detect bleeds authoritatively via Spell.dbc mechanics
Add a GetSpellSchool bleed check (Priority 0.5) backed by DBC SpellMechanic
data instead of damage-event learning / name patterns:
- GetSpellMechanicByID == 15 catches spell-level bleeds (Garrote, Rupture,
  Rend, Rip, Pounce, Deep Wounds).
- New ClassicAPI GetSpellEffectMechanics catches effect-level bleeds that the
  spell-level field misses -- Rake is spell-level 0 with EffectMechanic[2]=15.

Wrap C_Spell.GetSpellEffectMechanics in ClassicAPI.lua (nil-guarded so older
builds fall back to spell-level only). Verified against the client Spell.dbc.

Also drop Hemorrhage from the bleed name-pattern fallback: the DBC gives it no
bleed mechanic (physical damage), so bleed-immune mobs don't resist it -- it was
a false positive. Existing learning / split-damage / patterns remain as fallback.
2026-07-26 15:40:26 -05:00
Brues 2ce4d11698 Use ClassicAPI EventUtil for addon-load/login extension wiring
Replace hand-rolled ADDON_LOADED/PLAYER_LOGIN handlers with
EventUtil.ContinueOnAddOnLoaded / ContinueOnPlayerLogin, which fire
immediately if the event already happened -- removing the "we loaded before
the target addon and missed its ADDON_LOADED" workarounds.

- pfUI compat: ContinueOnAddOnLoaded("pfUI") + ContinueOnPlayerLogin; drops
  the missed-event fallback (login path still re-runs SetupCompatibility).
- MacroErrorUI / MacroLengthWarn: ContinueOnAddOnLoaded("Blizzard_MacroUI"),
  folding their manual "already loaded" checks.
- 9 Mouseover extensions (ag_UnitFrames, CT_RaidAssist, CT_UnitFrames,
  DiscordUnitFrames, Grid, NotGrid, Cursive, sRaidFrames, PerfectRaid):
  ContinueOnAddOnLoaded("<AddonName>", OnLoad). Since immediate-fire passes no
  event args, the old `arg1 == "X"` checks are replaced by the addon-name gate
  (global guards kept where present); also removes the buggy
  UnregisterEvent("ADDON_LOADED", "Onload") no-ops that never fired.

Addon names match file names; drops support for renamed folders (e.g. -master).
2026-07-26 14:53:34 -05:00
Brues e594fda295 Use C_Spell.GetSpellTexture in GetCachedIcon 2026-07-26 14:20:14 -05:00
Brues 22633aa16c Replace GetSpellRecField name/rank reads with C_Spell equivalents
Swap 166 call sites from GetSpellRecField(id, "name") to
C_Spell.GetSpellName(id) and GetSpellRecField(id, "rank") to
C_Spell.GetSpellSubtext(id) across Core, Conditionals, Utility,
ComboPointTracker, CursiveCustomSpells, pfUI, OverflowBuffFrame, and
Generic. Guard forms and the _GetSpellRecField alias calls collapse to the
direct C_Spell call.

GetSpellRecField stays for fields with no C_Spell equivalent (school,
spellIconID, mechanic, effectMechanic, effectApplyAuraName, stackAmount,
rangeIndex) and inside the NampowerAPI wrapper layer.
2026-07-26 14:07:31 -05:00
Brues 6718a01d79 Add [mounted]/[nomounted] and [standing]/[sitting] conditionals
Back them with ClassicAPI IsMounted() and UnitStandState("player").
mounted/nomounted are player mount state; standing = stand state 0,
sitting = any non-standing pose (its complement). Player-only, registered
in Keywords, BOOLEAN_CONDITIONALS, and STATIC_CONDITIONALS like [stealth].
2026-07-26 13:44:16 -05:00
Brues 1770f7b6e1 Make SendChatMessage reclaim cycle-safe and cover LVL
The previous reclaim stack-overflowed when LeafVillageAchievements loaded
after us (its captured "original" was our own function, so reclaiming onto
the top created an ours<->LVA loop). Add a reentrancy guard plus a base
SendChatMessage reference so a hook cycle routes straight to the base
function instead of recursing.

Gate the reclaim on LeafVillageAchievements OR LeafVillageLegends (both hook
SendChatMessage and can bump ours off the top -- LVA orphans it, LVL wraps
and re-installs on timers). Keep the anchored ^#showtooltip match since being
on top means we see the pristine line.
2026-07-24 14:09:36 -05:00
25 changed files with 980 additions and 849 deletions
+181 -8
View File
@@ -52,13 +52,13 @@ end
-- C_UnitAuras
--------------------------------------------------------------------------------
-- Scan a unit's auras (indexFn = C_UnitAuras.GetBuffDataByIndex or
-- GetDebuffDataByIndex) for one matching the dispel type. Vanilla descriptors
-- hold 32 helpful / 16 harmful slots; cap as a backstop.
local function scanDispel(indexFn, unit, dispelType, wantAny)
-- Scan one aura range of `unit` (filter = "HELPFUL" or "HARMFUL") for an aura
-- matching the dispel type. The filtered index self-terminates at the end of the
-- range (nil); 48 is a backstop over vanilla's 32 helpful / 16 harmful slots.
local function scanDispel(unit, filter, dispelType, wantAny)
local i = 1
while i <= 48 do
local data = indexFn(unit, i)
local data = C_UnitAuras.GetAuraDataByIndex(unit, i, filter)
if not data then return false end
local dn = data.dispelName
if dn and dn ~= "" and (wantAny or dn == dispelType) then
@@ -76,10 +76,29 @@ end
-- false -> scan debuffs (defensive cleanse, default)
function API.UnitHasDispelType(unit, dispelType, helpful)
if not unit or not UnitExists(unit) then return false end
local indexFn = helpful and C_UnitAuras.GetBuffDataByIndex
or C_UnitAuras.GetDebuffDataByIndex
local filter = helpful and "HELPFUL" or "HARMFUL"
local wantAny = (dispelType == nil or dispelType == "any")
return scanDispel(indexFn, unit, dispelType, wantAny)
return scanDispel(unit, filter, dispelType, wantAny)
end
-- First matching aura on `unit` by spellID, or nil. With no filter it walks the
-- whole aura array (helpful then harmful), so it finds a debuff even when it has
-- overflowed into an NPC's buff slots -- no 16+32 slot scan, no UnitIsPlayer
-- gate. filter ("HELPFUL"/"HARMFUL") restricts the search. Returns the modern
-- AuraData (spellId, name, applications, duration, expirationTime, dispelName, ...).
function API.GetUnitAuraBySpellID(unit, spellID, filter)
if not unit or not spellID then return nil end
return C_UnitAuras.GetUnitAuraBySpellID(unit, spellID, filter)
end
-- First matching aura on `unit` by spell NAME, or nil. Same whole-array search as
-- GetUnitAuraBySpellID; the name is case-sensitive and locale-resolved, so pass it
-- in the client's locale (what C_Spell.GetSpellName returns). filter
-- ("HELPFUL"/"HARMFUL") restricts the search. Prefer the by-ID variant for
-- portability where a spellID is known.
function API.GetAuraDataBySpellName(unit, spellName, filter)
if not unit or not spellName or spellName == "" then return nil end
return C_UnitAuras.GetAuraDataBySpellName(unit, spellName, filter)
end
--------------------------------------------------------------------------------
@@ -155,6 +174,76 @@ function API.GetItemSetInfo(setID)
return C_Item.GetItemSetInfo(setID)
end
--------------------------------------------------------------------------------
-- Equipment Set
--------------------------------------------------------------------------------
-- True if the saved equipment set named `name` is currently equipped -- every
-- resolvable item in its target slot (a missing bank-stored piece doesn't
-- disqualify, matching C_EquipmentSet.GetEquipmentSetInfo's isEquipped). Name is
-- an exact, case-sensitive match per GetEquipmentSetID. Returns false for an
-- unknown name or a client without the EquipmentSet API. Powers [equipset]/
-- [noequipset]; the swap side is the reclaimed /equipset command.
function API.IsEquipmentSetEquipped(name)
if not name or name == "" then return false end
local setID = C_EquipmentSet.GetEquipmentSetID(name)
if not setID then return false end
local _, _, _, isEquipped = C_EquipmentSet.GetEquipmentSetInfo(setID)
return isEquipped and true or false
end
--------------------------------------------------------------------------------
-- Weapon Enchant
--------------------------------------------------------------------------------
-- Temporary weapon-enchant state for a slot: "mh" (main), "oh" (off), or
-- "ranged". Returns hasEnchant, expirationMs, charges, enchantID. The enchantID
-- comes from ClassicAPI's modern C_Item.GetWeaponEnchantInfo 12-tuple (the
-- vanilla global omits it), so [mhenchant] can tell WHICH imbue is applied, not
-- just that one exists. Falls back to the vanilla 6-tuple global (enchantID nil,
-- no ranged slot) when the C_Item version is unavailable.
function API.GetWeaponEnchant(slot)
local hasM, mExp, mChg, mID, hasO, oExp, oChg, oID, hasR, rExp, rChg, rID =
C_Item.GetWeaponEnchantInfo()
if slot == "oh" then
return hasO, oExp, oChg, oID
elseif slot == "ranged" then
return hasR, rExp, rChg, rID
end
return hasM, mExp, mChg, mID
end
-- Localized name of an item-enchant ID (poison/oil/sharpening stone/permanent),
-- read straight from SpellItemEnchantment.dbc via ClassicAPI -- or nil for an
-- unknown id / a client without C_Item.GetEnchantInfo. Lets [mhenchant:Name]
-- resolve the applied enchant's name without scraping the weapon tooltip.
function API.GetEnchantName(enchantID)
if not enchantID or enchantID == 0 then return nil end
local info = C_Item.GetEnchantInfo(enchantID)
return info and info.name or nil
end
--------------------------------------------------------------------------------
-- Loss Of Control
--------------------------------------------------------------------------------
-- Locked spell-school mask from an active SCHOOL_INTERRUPT (Counterspell / Kick /
-- Pummel / Earth Shock lockout) on the player, or 0 when not kicked. Read from
-- C_LossOfControl, which synthesizes the lockout from the server's own
-- SMSG_SPELL_COOLDOWN packet -- a state no debuff scan can see. Also returns the
-- seconds remaining (nil if ClassicAPI didn't observe the applying cast). Returns
-- 0 for a client without C_LossOfControl. Player-only (vanilla LoC is local-only).
function API.GetSchoolLockout()
local n = C_LossOfControl.GetActiveLossOfControlDataCount() or 0
for i = 1, n do
local d = C_LossOfControl.GetActiveLossOfControlData(i)
if d and d.locType == "SCHOOL_INTERRUPT" then
return d.lockoutSchool or 0, d.timeRemaining
end
end
return 0, nil
end
--------------------------------------------------------------------------------
-- Spell
--------------------------------------------------------------------------------
@@ -168,6 +257,67 @@ function API.GetSpellMechanicByID(spellID)
return C_Spell.GetSpellMechanicByID(spellID)
end
-- Per-effect SpellMechanic ids (Spell.dbc EffectMechanic[3]) as {m1, m2, m3},
-- or nil for an invalid spell / 0 for an effect with no mechanic. Complements
-- GetSpellMechanicByID, which only reads the spell-level Mechanic field: vanilla
-- stores some mechanics on an effect instead (e.g. Rake's bleed is effect-level,
-- so GetSpellMechanicByID returns 0 but this returns {0,15,0}). Nil-guarded so an
-- older ClassicAPI build without the function degrades gracefully.
function API.GetSpellEffectMechanics(spellID)
return C_Spell.GetSpellEffectMechanics(spellID)
end
-- Flat spell-damage bonus (spell power) for a magic school, as a number.
-- school is 1-based: 1=Physical, 2=Holy, 3=Fire, 4=Nature, 5=Frost, 6=Shadow,
-- 7=Arcane. Reads the same client field nampower's GetSpellPower does -- exact,
-- with gear/enchants/buffs/talents/set bonuses baked in.
function API.GetSpellBonusDamage(school)
return GetSpellBonusDamage(school)
end
-- Flat healing bonus (+healing), as a number. Vanilla has no healing-done field,
-- so ClassicAPI derives it from gear/enchant/buff MOD_HEALING_DONE plus
-- stat-conversion talents (e.g. Spiritual Guidance) -- exact, not a holy-damage
-- proxy.
function API.GetSpellBonusHealing()
return GetSpellBonusHealing()
end
--------------------------------------------------------------------------------
-- Unit Health
--------------------------------------------------------------------------------
-- Health deficit (max - current) for `unit` in one call. Falls back to
-- UnitHealthMax - UnitHealth without ClassicAPI.
API.UnitHealthMissing = UnitHealthMissing or function(unit)
return (UnitHealthMax(unit) or 0) - (UnitHealth(unit) or 0)
end
--------------------------------------------------------------------------------
-- Unit Power
--------------------------------------------------------------------------------
-- Current power for a specific Enum.PowerType (0=Mana, 1=Rage, 2=Focus,
-- 3=Energy, 4=Happiness), or the unit's primary power when powerType is omitted.
-- Display-divided (rage reads 0..100). Falls back to UnitMana without ClassicAPI.
API.UnitPower = UnitPower or function(unit, powerType)
return UnitMana(unit)
end
API.UnitPowerMax = UnitPowerMax or function(unit, powerType)
return UnitManaMax(unit)
end
-- Power deficit (max - current) for the type / primary power, in one call.
API.UnitPowerMissing = UnitPowerMissing or function(unit, powerType)
return (UnitManaMax(unit) or 0) - (UnitMana(unit) or 0)
end
-- Unit's primary power type as an integer (0=Mana .. 4=Happiness).
function API.UnitPowerType(unit)
return UnitPowerType(unit)
end
--------------------------------------------------------------------------------
-- State
--------------------------------------------------------------------------------
@@ -182,6 +332,29 @@ function API.IsSwimming()
return IsSwimming() and true or false
end
-- True if the player is currently mounted.
function API.IsMounted()
return IsMounted() and true or false
end
-- True if the player is under a WMO roof (building, cave, instance interior).
-- Live engine geometry query, not zone-based; nil (-> false) pre-world.
function API.IsIndoors()
return IsIndoors() and true or false
end
-- True if the player is outdoors (open sky / not inside a WMO interior).
-- Exact complement of IsIndoors for a resolvable player.
function API.IsOutdoors()
return IsOutdoors() and true or false
end
-- Player's stand state: 0 = standing, non-zero = sitting/sleeping/kneeling/etc.
-- (see UnitStandState). Player-only.
function API.GetPlayerStandState()
return UnitStandState("player") or 0
end
--------------------------------------------------------------------------------
-- Cursor
--------------------------------------------------------------------------------
+40 -67
View File
@@ -32,6 +32,35 @@ CleveRoids.lastRakeCast = CleveRoids.lastRakeCast or {
timestamp = 0
}
-- Family membership without hardcoded rank lists. C_Spell.GetSpellName resolves
-- ANY spellID from the client's Spell.dbc -- every rank (so no enumeration or
-- spellbook scan) and TWoW's custom spells alike -- so "is this spellID a Rip?"
-- is just a name match against one seed rank. seedNameCache memoizes the seed's
-- localized name (locale-safe: derived from the ID, not a hardcoded string; a
-- seed absent from the DBC, e.g. a TWoW spell on a stock client, resolves to nil
-- and the set simply never matches -- which is correct, that spell can't be cast).
local seedNameCache = {}
local function SeedName(seedID)
local n = seedNameCache[seedID]
if n == nil then
n = C_Spell.GetSpellName(seedID) or false
seedNameCache[seedID] = n
end
return n or nil
end
-- Stand-in for a hardcoded {spellID=true,...} rank set: t[spellID] is true iff
-- spellID is any rank of the family seeded by seedID. Same read shape as the old
-- tables, so every membership consumer (`X and X[id]`) keeps working unchanged.
-- Only valid for membership tests -- these are not iterable (pairs() sees empty).
local function RankSet(seedID)
return setmetatable({}, { __index = function(_, spellID)
if type(spellID) ~= "number" then return nil end
local name = SeedName(seedID)
return (name and C_Spell.GetSpellName(spellID) == name) and true or nil
end })
end
-- Define spells that scale with combo points by SPELL ID and their duration formulas
-- Duration = base + (combo_points - 1) * increment
CleveRoids.ComboScalingSpellsByID = {
@@ -70,32 +99,9 @@ CleveRoids.FerociousBiteSpellIDs = {
[31018] = true, -- Rank 6
}
-- Rip spell IDs (for Carnage talent - refreshed when Carnage procs)
CleveRoids.RipSpellIDs = {
[1079] = true, -- Rank 1
[9492] = true, -- Rank 2
[9493] = true, -- Rank 3
[9752] = true, -- Rank 4
[9894] = true, -- Rank 5
[9896] = true, -- Rank 6
}
-- Rake spell IDs (for Carnage talent - refreshed when Carnage procs)
CleveRoids.RakeSpellIDs = {
[1822] = true, -- Rank 1
[1823] = true, -- Rank 2
[1824] = true, -- Rank 3
[9904] = true, -- Rank 4
}
-- Pounce Bleed spell IDs (for immunity detection - bleed portion of Pounce)
-- Note: Pounce (cast) TRIGGERS a separate Pounce Bleed spell with different IDs
-- Cast IDs: 9005, 9823, 9827 → Trigger Bleed IDs: 9007, 9824, 9826
CleveRoids.PounceBleedSpellIDs = {
[9007] = true, -- Rank 1 (triggered by Pounce 9005)
[9824] = true, -- Rank 2 (triggered by Pounce 9823)
[9826] = true, -- Rank 3 (triggered by Pounce 9827)
}
-- Rip / Rake families (for Carnage talent). Seeded by Rank 1; matches every rank.
CleveRoids.RipSpellIDs = RankSet(1079)
CleveRoids.RakeSpellIDs = RankSet(1822)
-- Combined table for all bleed spells that need immunity detection
-- Used when checking if a cast bleed failed to apply (indicates bleed immunity)
@@ -132,14 +138,9 @@ CleveRoids.PounceToBleedMapping = {
-- When Molten Blast hits, it refreshes Flame Shock duration on the target
-- Detection: Monitor combat log for Molten Blast damage, then refresh Flame Shock
-- =============================================================================
CleveRoids.MoltenBlastSpellIDs = {
[36916] = true, -- Rank 1
[36917] = true, -- Rank 2
[36918] = true, -- Rank 3
[36919] = true, -- Rank 4
[36920] = true, -- Rank 5
[36921] = true, -- Rank 6
}
-- TWoW custom; seed resolves on the TWoW client (nil/never-matches on stock,
-- where Molten Blast can't be cast anyway).
CleveRoids.MoltenBlastSpellIDs = RankSet(36916)
CleveRoids.FlameShockSpellIDs = {
[8050] = true, -- Rank 1
@@ -154,12 +155,7 @@ CleveRoids.FlameShockSpellIDs = {
-- WARLOCK: Conflagrate → Immolate Duration Reduction
-- When Conflagrate is cast, it reduces Immolate duration by 3 seconds
-- =============================================================================
CleveRoids.ConflagrateSpellIDs = {
[17962] = true, -- Rank 1
[18930] = true, -- Rank 2
[18931] = true, -- Rank 3
[18932] = true, -- Rank 4
}
CleveRoids.ConflagrateSpellIDs = RankSet(17962)
CleveRoids.ImmolateSpellIDs = {
[348] = true, -- Rank 1
@@ -177,11 +173,8 @@ CleveRoids.ImmolateSpellIDs = {
-- Channeled spell that accelerates DoT tick rate by 30% while channeling
-- Complex tracking: debuff expires 30% faster while Dark Harvest is active
-- =============================================================================
CleveRoids.DarkHarvestSpellIDs = {
[52550] = true, -- Rank 1
[52551] = true, -- Rank 2
[52552] = true, -- Rank 3
}
-- TWoW custom (see MoltenBlast note).
CleveRoids.DarkHarvestSpellIDs = RankSet(52550)
-- =============================================================================
-- DRUID: Rake Debuff Cap Boss Whitelist
@@ -489,7 +482,7 @@ function CleveRoids.TrackComboPointCastByID(spellID, targetGUID)
end
else
-- Second, check if name-based tracking has recent data for this spell
local spellName = GetSpellRecField(spellID, "name")
local spellName = C_Spell.GetSpellName(spellID)
if spellName then
-- Remove rank info for comparison
local baseName = CleveRoids.StripRank(spellName)
@@ -548,26 +541,6 @@ function CleveRoids.TrackComboPointCastByID(spellID, targetGUID)
return duration
end
-- API function to get last tracked combo points for a spell
function CleveRoids.GetLastComboPointsForSpell(spellName)
if CleveRoids.ComboPointTracking[spellName] then
return CleveRoids.ComboPointTracking[spellName].combo_points
elseif CleveRoids.spell_tracking[spellName] then
return CleveRoids.spell_tracking[spellName].last_combo_points
end
return nil
end
-- API function to get last calculated duration for a spell
function CleveRoids.GetLastDurationForSpell(spellName)
if CleveRoids.ComboPointTracking[spellName] then
return CleveRoids.ComboPointTracking[spellName].duration
elseif CleveRoids.spell_tracking[spellName] then
return CleveRoids.spell_tracking[spellName].last_duration
end
return nil
end
-- Utility function to display current combo tracking info
function CleveRoids.ShowComboTracking()
CleveRoids.Print("=== Combo Point Tracking ===")
@@ -683,7 +656,7 @@ if _G.UseAction then
local spellName = nil
if actionType == "SPELL" and actionID then
spellName = GetSpellRecField(actionID, "name")
spellName = C_Spell.GetSpellName(actionID)
end
if currentCP and currentCP > 0 then
+25 -19
View File
@@ -2,9 +2,10 @@ local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
local Extension = CleveRoids.RegisterExtension("Compatibility_pfUI")
Extension.RegisterEvent("ADDON_LOADED", "ADDON_LOADED")
Extension.RegisterEvent("PLAYER_LOGIN", "PLAYER_LOGIN")
Extension.Debug = false
-- pfUI-loaded and player-login handlers are wired via ClassicAPI's EventUtil at
-- the bottom of the file (ContinueOnAddOnLoaded fires immediately if pfUI already
-- loaded, so no separate "we missed pfUI's ADDON_LOADED" fallback is needed).
-- Track pfUI state
Extension.pfUILoaded = false
@@ -99,7 +100,7 @@ local function GetCarnageOverride(effect)
end
for spellID, override in pairs(CleveRoids.carnageDurationOverrides) do
local spellName = GetSpellRecField(spellID, "name")
local spellName = C_Spell.GetSpellName(spellID)
if spellName then
local baseName = CleveRoids.StripRank(spellName)
if baseName == effect and override.timestamp and (GetTime() - override.timestamp) < 5 then
@@ -209,7 +210,7 @@ function Extension.HookPfUILibdebuff()
for spellID, rec in pairs(CleveRoids.libdebuff.objects[unitGUID]) do
if rec and rec.start and rec.duration then
-- Get spell name for this ID
local spellName = GetSpellRecField(spellID, "name")
local spellName = C_Spell.GetSpellName(spellID)
if spellName then
local baseName = CleveRoids.StripRank(spellName)
if baseName == effect then
@@ -380,7 +381,7 @@ function Extension.SyncComboDurationToPfUI(guid, spellID, duration)
end
-- Get spell name from spell ID
local spellName = GetSpellRecField(spellID, "name")
local spellName = C_Spell.GetSpellName(spellID)
if not spellName then
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[pfUI Sync]|r Could not find spell name for ID " .. spellID)
@@ -726,23 +727,22 @@ function Extension.OnLoad()
SLASH_PFUICD1 = "/pfuicd"
end
function Extension.ADDON_LOADED()
-- Check if pfUI just loaded AND the global actually exists
-- (another addon could be named "pfUI" without being the real UI framework)
if arg1 == "pfUI" and pfUI then
Extension.pfUILoaded = true
-- pfUI modules load after ADDON_LOADED, so schedule a check
if CleveRoids.ScheduleTimer then
CleveRoids.ScheduleTimer(function()
Extension.SetupCompatibility()
end, 0.5)
end
-- Fires once pfUI has loaded (immediately if it loaded before us, via EventUtil).
function Extension.OnPfUILoaded()
-- Guard: only the real pfUI framework sets this global (another addon could be
-- named "pfUI" without being the UI framework).
if not pfUI then return end
Extension.pfUILoaded = true
-- pfUI's submodules initialize after its ADDON_LOADED, so defer the setup.
if CleveRoids.ScheduleTimer then
CleveRoids.ScheduleTimer(function()
Extension.SetupCompatibility()
end, 0.5)
end
end
function Extension.PLAYER_LOGIN()
-- If pfUI loaded before SCRM (alphabetical order), ADDON_LOADED for pfUI was missed.
-- Re-run InitPfUIIntegration here to ensure lib.objects is linked correctly.
function Extension.OnPlayerLogin()
-- Ensure lib.objects is linked correctly (InitPfUIIntegration is idempotent).
if pfUI and not CleveRoids.hasPfUI76 then
local lib = CleveRoids.libdebuff
if lib and lib.InitPfUIIntegration then
@@ -797,4 +797,10 @@ if not CleveRoids.ScheduleTimer then
end
end
-- Wire handlers via ClassicAPI EventUtil (fires immediately if the event already
-- happened, so load order relative to pfUI no longer matters). Registered here,
-- after the handlers are defined, since ContinueOnAddOnLoaded may fire inline.
EventUtil.ContinueOnAddOnLoaded("pfUI", Extension.OnPfUILoaded)
EventUtil.ContinueOnPlayerLogin(Extension.OnPlayerLogin)
_G["CleveRoids"] = CleveRoids
+314 -221
View File
@@ -55,15 +55,14 @@ function CleveRoids.GetCachedPlayerHealthPercent()
end
function CleveRoids.GetCachedPlayerPowerPercent()
local API = CleveRoids.NampowerAPI
local power = API and API.GetUnitPower and API.GetUnitPower("player") or UnitMana("player")
local max = API and API.GetUnitMaxPower and API.GetUnitMaxPower("player") or UnitManaMax("player")
local API = CleveRoids.ClassicAPI
local power = API.UnitPower("player")
local max = API.UnitPowerMax("player")
return max > 0 and (100 * power / max) or 0
end
function CleveRoids.GetCachedPlayerPower()
local API = CleveRoids.NampowerAPI
return API and API.GetUnitPower and API.GetUnitPower("player") or UnitMana("player")
return CleveRoids.ClassicAPI.UnitPower("player")
end
function CleveRoids.GetCachedTargetHealthPercent()
@@ -105,7 +104,7 @@ local function BuildSpellNameCache()
if lib.personalDebuffs then
for sid, _ in pairs(lib.personalDebuffs) do
local name = GetSpellRecField(sid, "name")
local name = C_Spell.GetSpellName(sid)
if name then
name = CleveRoids.StripRank(name)
if not _spellNameToIDs[name] then
@@ -118,7 +117,7 @@ local function BuildSpellNameCache()
if lib.sharedDebuffs then
for sid, _ in pairs(lib.sharedDebuffs) do
local name = GetSpellRecField(sid, "name")
local name = C_Spell.GetSpellName(sid)
if name then
name = CleveRoids.StripRank(name)
if not _spellNameToIDs[name] then
@@ -163,7 +162,7 @@ local function GetSpellIDForRank(baseName, rankNum)
if not matchIDs then return nil end
local targetRank = "Rank " .. rankNum
for _, sid in ipairs(matchIDs) do
local rank = GetSpellRecField and GetSpellRecField(sid, "rank")
local rank = C_Spell.GetSpellSubtext(sid)
if rank and rank == targetRank then
return sid
end
@@ -198,102 +197,6 @@ local function IsSharedDebuffByIdOrName(lib, spellID, debuffName)
return false
end
-- PERFORMANCE: Equipment cache for HasGearEquipped (avoids 19-slot scan per call)
-- Invalidated on UNIT_INVENTORY_CHANGED via CleveRoids.InvalidateEquipmentCache()
-- Enhanced with Nampower v2.18+ GetEquippedItems when available
local _equippedItemIDs = {} -- [slot] = itemID (number)
local _equippedItemNames = {} -- [slot] = itemName (lowercase string)
local _equipmentCacheValid = false
-- Track if we've warned about GetEquippedItems errors (warn once per session)
local _getEquippedItemsErrorWarned = false
local function BuildEquipmentCache()
if _equipmentCacheValid then return end
_equipmentCacheValid = true
-- Clear old data
for i = 1, 19 do
_equippedItemIDs[i] = nil
_equippedItemNames[i] = nil
end
local string_find = string.find
local string_lower = string.lower
-- Try Nampower GetEquippedItems for faster enumeration
-- Requires v2.22+ because earlier versions (e.g., v2.19.1) have internal bug
local API = CleveRoids.NampowerAPI
local hasValidNampower = API and API.HasMinimumVersion and API.HasMinimumVersion(2, 22, 0)
if hasValidNampower and GetEquippedItems then
-- Use pcall to catch any internal Nampower errors and fall back gracefully
local success, result = pcall(GetEquippedItems, "player")
if not success then
-- Log the error once per session for debugging
if not _getEquippedItemsErrorWarned then
_getEquippedItemsErrorWarned = true
local errMsg = tostring(result)
if CleveRoids.Print then
CleveRoids.Print("|cffff6600Warning:|r GetEquippedItems failed: " .. errMsg)
CleveRoids.Print("Using fallback equipment detection. Consider updating Nampower.")
end
end
-- Fall through to manual enumeration
elseif result and type(result) == "table" then
local usedNampower = false
for nampowerSlot, itemInfo in pairs(result) do
-- Nampower uses 0-indexed slots, WoW API uses 1-indexed
-- tonumber() handles both string and numeric keys from different Nampower versions
-- Skip non-numeric keys (metadata fields, etc.)
local slotNum = tonumber(nampowerSlot)
if slotNum and type(itemInfo) == "table" and itemInfo.itemId then
local slot = slotNum + 1
-- itemInfo must be a table to access .itemId (userdata from some Nampower versions is not indexable)
if slot >= 1 and slot <= 19 then
_equippedItemIDs[slot] = itemInfo.itemId
usedNampower = true
-- Get item name via Nampower API or GetItemInfo
local itemName = API and API.GetItemName and API.GetItemName(itemInfo.itemId)
if not itemName then
itemName = GetItemInfo(itemInfo.itemId)
end
if itemName then
_equippedItemNames[slot] = string_lower(itemName)
end
end
end
end
if usedNampower then
return -- Done with Nampower path
end
-- Fall through to manual enumeration if Nampower returned userdata items
end
end
-- Fallback: manual slot enumeration via ClassicAPI (id + decorated name),
-- no link string built. C_Item.GetItemName carries random-suffix decoration
-- and falls back to the base name internally, so it replaces the old
-- bracket-name / GetItemInfo two-step in a single call.
for slot = 1, 19 do
local id = GetInventoryItemID("player", slot)
if id then
_equippedItemIDs[slot] = id
local name = C_Item.GetItemName({ equipmentSlotIndex = slot })
if name then
_equippedItemNames[slot] = string_lower(name)
end
end
end
end
-- Invalidate equipment cache (call on UNIT_INVENTORY_CHANGED)
function CleveRoids.InvalidateEquipmentCache()
_equipmentCacheValid = false
end
-- ============================================================================
-- PERFORMANCE: Unified item location lookup using CleveRoids.Items cache
-- Returns: { type="inventory"|"bag", inventoryID=N } or { type="bag", bag=N, slot=N }
@@ -444,24 +347,26 @@ local stat_checks = {
attackpower = function() local base, pos, neg = UnitAttackPower("player"); return base + pos + neg end,
rap = function() local base, pos, neg = UnitRangedAttackPower("player"); return base + pos + neg end,
rangedattackpower = function() local base, pos, neg = UnitRangedAttackPower("player"); return base + pos + neg end,
healing = function() local _, h = CleveRoids.NampowerAPI.GetSpellPower(); return h or 0 end,
healingpower = function() local _, h = CleveRoids.NampowerAPI.GetSpellPower(); return h or 0 end,
healing = function() return CleveRoids.ClassicAPI.GetSpellBonusHealing() or 0 end,
healingpower = function() return CleveRoids.ClassicAPI.GetSpellBonusHealing() or 0 end,
-- Bonus Spell Damage by School (Nampower v2.31+ GetSpellPower)
-- GetSpellPower() returns: physical, holy, fire, nature, frost, shadow, arcane
arcane_power = function() return select(7, CleveRoids.NampowerAPI.GetSpellPower()) or 0 end,
fire_power = function() return select(3, CleveRoids.NampowerAPI.GetSpellPower()) or 0 end,
frost_power = function() return select(5, CleveRoids.NampowerAPI.GetSpellPower()) or 0 end,
nature_power = function() return select(4, CleveRoids.NampowerAPI.GetSpellPower()) or 0 end,
shadow_power = function() return select(6, CleveRoids.NampowerAPI.GetSpellPower()) or 0 end,
-- Bonus Spell Damage by School (ClassicAPI GetSpellBonusDamage)
-- school: 1=Physical, 2=Holy, 3=Fire, 4=Nature, 5=Frost, 6=Shadow, 7=Arcane
arcane_power = function() return CleveRoids.ClassicAPI.GetSpellBonusDamage(7) or 0 end,
fire_power = function() return CleveRoids.ClassicAPI.GetSpellBonusDamage(3) or 0 end,
frost_power = function() return CleveRoids.ClassicAPI.GetSpellBonusDamage(5) or 0 end,
nature_power = function() return CleveRoids.ClassicAPI.GetSpellBonusDamage(4) or 0 end,
shadow_power = function() return CleveRoids.ClassicAPI.GetSpellBonusDamage(6) or 0 end,
-- Highest spell power across all schools
spell_power = function()
local p, h, fi, n, fr, s, a = CleveRoids.NampowerAPI.GetSpellPower()
if p then
return math.max(p, h, fi, n, fr, s, a)
local API = CleveRoids.ClassicAPI
local best = 0
for s = 1, 7 do
local v = API.GetSpellBonusDamage(s) or 0
if v > best then best = v end
end
return 0
return best
end,
-- Defensive Stats
@@ -916,7 +821,7 @@ function CleveRoids.GetAllCasterAuraTimeRemaining(targetGuid, spellId)
local targetData, isPfUI = CleveRoids.GetAuraTrackingData(targetGuid)
if not targetData then return nil end
local spellName = GetSpellRecField and GetSpellRecField(spellId, "name")
local spellName = C_Spell.GetSpellName(spellId)
if not spellName then return nil end
local casters = targetData[spellName]
@@ -960,7 +865,7 @@ function CleveRoids.FindAllCasterAuraByName(targetGuid, searchName)
-- Resolve spell ID to name for direct lookup
local searchID = tonumber(searchName)
if searchID then
local resolvedName = GetSpellRecField and GetSpellRecField(searchID, "name")
local resolvedName = C_Spell.GetSpellName(searchID)
if not resolvedName then return nil, nil end
searchName = resolvedName
end
@@ -1081,7 +986,7 @@ local function OnAutoAttackOther(attackerGuid, targetGuid, totalDamage, hitInfo,
rec.start = GetTime()
if CleveRoids.debug then
local spellName = GetSpellRecField and GetSpellRecField(spellID, "name") or "Unknown"
local spellName = C_Spell.GetSpellName(spellID) or "Unknown"
local baseName = CleveRoids.StripRank(spellName) or "Unknown"
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ffaa[Judgement Refresh]|r Refreshed %s (ID:%d) on melee hit - new duration: %ds",
@@ -1091,7 +996,7 @@ local function OnAutoAttackOther(attackerGuid, targetGuid, totalDamage, hitInfo,
-- Sync to pfUI if loaded (pre-7.6 only)
if not CleveRoids.hasPfUI76 and pfUI and pfUI.api and pfUI.api.libdebuff then
local spellName = GetSpellRecField and GetSpellRecField(spellID, "name") or nil
local spellName = C_Spell.GetSpellName(spellID) or nil
local baseName = CleveRoids.StripRank(spellName)
local targetName = (lib.guidToName and lib.guidToName[normalizedTarget]) or UnitName("target")
local targetLevel = UnitLevel("target") or 0
@@ -1236,7 +1141,7 @@ local function OnAuraCastSelf(spellId, casterGuid, targetGuid, effect, effectAur
-- Use the isBuffNotDebuff result determined above (avoids redundant slot scanning)
local lib = CleveRoids.libdebuff
if isBuffNotDebuff and spellId and durationMs and durationMs > 0 and lib and not lib.hasPfUIEnhanced then
local spellName = GetSpellRecField and GetSpellRecField(spellId, "name")
local spellName = C_Spell.GetSpellName(spellId)
if spellName then
local playerGuid = CleveRoids.GetGUID("player")
if playerGuid then
@@ -1269,7 +1174,7 @@ local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAu
-- When pfUI enhanced is active, pfUI writes to pfUI.libdebuff_all_auras with
-- full downrank protection — we read from that table via GetAuraTrackingData().
if spellId and durationMs and durationMs > 0 then
local spellName = GetSpellRecField and GetSpellRecField(spellId, "name")
local spellName = C_Spell.GetSpellName(spellId)
if spellName and not CleveRoids.hasPfUI76 then
CleveRoids._allCasterAuraDirty = true
if not CleveRoids.AllCasterAuraTracking[targetGuid] then
@@ -1327,7 +1232,7 @@ local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAu
-- (AURA_CAST_ON_OTHER fires for both buffs and debuffs; BUFF_ADDED_OTHER confirms buff)
local lib = CleveRoids.libdebuff
if lib and not lib.hasPfUIEnhanced then
local spellNameForPending = GetSpellRecField and GetSpellRecField(spellId, "name")
local spellNameForPending = C_Spell.GetSpellName(spellId)
if spellNameForPending then
local normTargetGuid = CleveRoids.NormalizeGUID(targetGuid)
if normTargetGuid then
@@ -1472,7 +1377,7 @@ autoAttackFrame:SetScript("OnEvent", function()
if spellId and spellId > 0 and durationMs and durationMs > 0 then
local playerGUID = CleveRoids.GetGUID("player")
local durSpellName = GetSpellRecField and GetSpellRecField(spellId, "name")
local durSpellName = C_Spell.GetSpellName(spellId)
if playerGUID and durSpellName and not CleveRoids.hasPfUI76 then
CleveRoids._allCasterAuraDirty = true
if not CleveRoids.AllCasterAuraTracking[playerGUID] then
@@ -1524,7 +1429,7 @@ autoAttackFrame:SetScript("OnEvent", function()
local state = arg7
if state == 2 then return end -- Stack change, not full removal
if guid and spellId and CleveRoids.AllCasterAuraTracking[guid] then
local removedName = GetSpellRecField and GetSpellRecField(spellId, "name")
local removedName = C_Spell.GetSpellName(spellId)
if removedName and CleveRoids.AllCasterAuraTracking[guid][removedName] then
CleveRoids.AllCasterAuraTracking[guid][removedName] = nil
if not next(CleveRoids.AllCasterAuraTracking[guid]) then
@@ -1880,7 +1785,7 @@ local function IsPendingDebuffCast(spellName, targetUnit)
if arr then
for _, pending in pairs(arr) do
if pending and pending.targetGUID == targetGuid and pending.spellID then
local pendingName = GetSpellRecField and GetSpellRecField(pending.spellID, "name")
local pendingName = C_Spell.GetSpellName(pending.spellID)
if pendingName then
local normalizedPending = NormalizeSpellNameForComparison(pendingName)
if normalizedPending == normalizedCheck then
@@ -2018,7 +1923,7 @@ function CleveRoids.CancelAura(auraName)
if aura_ix == -1 then break end
local bid = GetPlayerBuffID(aura_ix)
bid = (bid < -1) and (bid + 65536) or bid
if string.lower(GetSpellRecField(bid, "name") or "") == auraName then
if string.lower(C_Spell.GetSpellName(bid) or "") == auraName then
C_Spell.CancelSpellByID(bid)
return true
end
@@ -2031,7 +1936,7 @@ function CleveRoids.CancelAura(auraName)
for slot = 0, 31 do
local spellId = _G.GetPlayerAuraDuration(slot)
if spellId and spellId > 0 then
local name = GetSpellRecField(spellId, "name")
local name = C_Spell.GetSpellName(spellId)
if name and string.lower(name) == auraName then
C_Spell.CancelSpellByID(spellId)
return true
@@ -2047,7 +1952,7 @@ function CleveRoids.CancelAura(auraName)
if entry.durationSec and entry.durationSec > 0 and elapsed > entry.durationSec then
CleveRoids.OverflowBuffs[spellId] = nil
else
local name = GetSpellRecField(spellId, "name")
local name = C_Spell.GetSpellName(spellId)
if name and string.lower(name) == auraName then
C_Spell.CancelSpellByID(spellId)
CleveRoids.OverflowBuffs[spellId] = nil
@@ -2059,26 +1964,12 @@ function CleveRoids.CancelAura(auraName)
return false
end
-- ClassicAPI's C_Item.IsEquippedItem walks the 19 equipment slots natively and
-- short-circuits on the first match, so it replaces the old Lua-side equipment
-- cache entirely -- no per-call scan, no invalidation. Accepts itemID, item
-- link, or (case-insensitive, decorated) name.
function CleveRoids.HasGearEquipped(gearId)
if not gearId then return false end
-- PERFORMANCE: Build/refresh equipment cache if needed
BuildEquipmentCache()
-- Handle both numeric IDs and string IDs like "5196"
local wantId = tonumber(gearId)
local wantName = (type(gearId) == "string" and not wantId) and string.lower(gearId) or nil
-- PERFORMANCE: Use cached data instead of scanning all slots
for slot = 1, 19 do
if wantId and _equippedItemIDs[slot] == wantId then
return true
end
if wantName and _equippedItemNames[slot] == wantName then
return true
end
end
return false
return (gearId and C_Item.IsEquippedItem(gearId)) or false
end
@@ -2952,11 +2843,8 @@ function CleveRoids.CountEnemiesMatching(checkFunc)
tryUnit("targettarget")
tryUnit("targettargettarget")
tryUnit("pettarget")
if pfUI and pfUI.uf and pfUI.uf.focus and pfUI.uf.focus.label and pfUI.uf.focus.id then
local focusUnit = pfUI.uf.focus.label .. pfUI.uf.focus.id
tryUnit(focusUnit)
tryUnit(focusUnit .. "target")
end
tryUnit("focus")
tryUnit("focustarget")
for i = 1, 4 do
tryUnit("party" .. i .. "target")
end
@@ -2966,22 +2854,8 @@ function CleveRoids.CountEnemiesMatching(checkFunc)
end
end
-- 3. Nameplate scan: visible nameplates give live GUIDs without target switching.
-- ClassicAPI's C_NamePlate.GetNamePlateGUIDs() lists every unit with an
-- allocated nameplate (including default vanilla nameplates), replacing the
-- old WorldFrame child-walk + frame:GetName(1) GUID-extraction.
local plateGuids = CleveRoids.ClassicAPI.GetNamePlateGUIDs()
if plateGuids then
for i = 1, table.getn(plateGuids) do
local guid = plateGuids[i]
if guid and not checked[guid] and UnitExists(guid) and UnitCanAttack("player", guid) then
checked[guid] = true
CleveRoids.knownEnemyGuids[guid] = true
if checkFunc(guid) then
count = count + 1
end
end
end
for i = 1, 40 do
tryUnit("nameplate"..i)
end
-- During tooltip evaluation, skip known-enemy cache iteration.
@@ -3297,9 +3171,9 @@ end
-- returns: True or false
function CleveRoids.ValidatePower(unit, operator, amount)
if not unit or not operator or not amount then return false end
local API = CleveRoids.NampowerAPI
local power = API and API.GetUnitPower and API.GetUnitPower(unit) or UnitMana(unit)
local maxPower = API and API.GetUnitMaxPower and API.GetUnitMaxPower(unit) or UnitManaMax(unit)
local API = CleveRoids.ClassicAPI
local power = API.UnitPower(unit)
local maxPower = API.UnitPowerMax(unit)
local powerPercent = maxPower > 0 and (100 * power / maxPower) or 0
if CleveRoids.operators[operator] then
@@ -3309,6 +3183,26 @@ function CleveRoids.ValidatePower(unit, operator, amount)
return false
end
-- Validates a SPECIFIC power slot (Enum.PowerType: 0=Mana, 1=Rage, 3=Energy) as a
-- percentage, regardless of the unit's primary power -- so [mana]/[energy]/[rage]
-- read the right pool cross-form (druid mana in cat) and cross-unit (@target mana).
-- A unit with no such pool (max <= 0) fails rather than reading as 0% (avoids a
-- rageless caster satisfying [rage:<10]).
function CleveRoids.ValidateTypedPower(unit, powerType, operator, amount)
if not unit or not operator or not amount then return false end
if not UnitExists(unit) then return false end
local API = CleveRoids.ClassicAPI
local maxPower = API.UnitPowerMax(unit, powerType)
if not maxPower or maxPower <= 0 then return false end
local powerPercent = 100 * API.UnitPower(unit, powerType) / maxPower
if CleveRoids.operators[operator] then
return CleveRoids.comparators[operator](powerPercent, amount)
end
return false
end
-- Checks whether or not the given unit has current power vs the given amount
-- unit: The unit we're checking
-- operator: valid comparitive operator symbol
@@ -3316,8 +3210,7 @@ end
-- returns: True or false
function CleveRoids.ValidateRawPower(unit, operator, amount)
if not unit or not operator or not amount then return false end
local API = CleveRoids.NampowerAPI
local power = API and API.GetUnitPower and API.GetUnitPower(unit) or UnitMana(unit)
local power = CleveRoids.ClassicAPI.UnitPower(unit)
if power and CleveRoids.operators[operator] then
return CleveRoids.comparators[operator](power, amount)
@@ -3326,23 +3219,15 @@ function CleveRoids.ValidateRawPower(unit, operator, amount)
return false
end
-- Raw caster-form mana for druids (SuperWoW: 2nd return of UnitMana)
-- Raw caster-form mana for druids. Read the mana power slot directly
-- (0 = Enum.PowerType.Mana) so it works while shapeshifted, when the druid's
-- primary power is rage/energy -- no SuperWoW 2nd-return-of-UnitMana needed.
function CleveRoids.ValidateDruidRawMana(unit, operator, amount)
unit = unit or "player"
if not operator or amount == nil then return false end
if (CleveRoids.playerClass ~= "DRUID") then return false end
-- SuperWoW returns: current-form power, caster-form mana
local _, casterMana = UnitMana(unit)
-- Fallback: if for some reason we didn't get a 2nd value and we're in caster form now
if type(casterMana) ~= "number" then
if UnitPowerType and UnitPowerType(unit) == 0 then
casterMana = UnitMana(unit)
else
return false
end
end
local casterMana = CleveRoids.ClassicAPI.UnitPower(unit, 0)
local cmp = CleveRoids.comparators and CleveRoids.comparators[operator]
return cmp and cmp(casterMana, amount) or false
@@ -3355,10 +3240,7 @@ end
-- returns: True or false
function CleveRoids.ValidatePowerLost(unit, operator, amount)
if not unit or not operator or not amount then return false end
local API = CleveRoids.NampowerAPI
local maxPower = API and API.GetUnitMaxPower and API.GetUnitMaxPower(unit) or UnitManaMax(unit)
local power = API and API.GetUnitPower and API.GetUnitPower(unit) or UnitMana(unit)
local powerLost = maxPower - power
local powerLost = CleveRoids.ClassicAPI.UnitPowerMissing(unit)
if CleveRoids.operators[operator] then
return CleveRoids.comparators[operator](powerLost, amount)
@@ -3410,10 +3292,7 @@ end
-- returns: True or false
function CleveRoids.ValidateHpLost(unit, operator, amount)
if not unit or not operator or not amount then return false end
local API = CleveRoids.NampowerAPI
local maxHp = API and API.GetUnitMaxHealth and API.GetUnitMaxHealth(unit) or UnitHealthMax(unit)
local hp = API and API.GetUnitHealth and API.GetUnitHealth(unit) or UnitHealth(unit)
local hpLost = maxHp - hp
local hpLost = CleveRoids.ClassicAPI.UnitHealthMissing(unit)
if CleveRoids.operators[operator] then
return CleveRoids.comparators[operator](hpLost, amount)
@@ -3530,7 +3409,7 @@ local function GetLowercaseSpellName(spellID)
local cached = _spellNameCache[spellID]
if cached then return cached end
local name = GetSpellRecField(spellID, "name")
local name = C_Spell.GetSpellName(spellID)
if not name then return nil end
-- Strip rank and lowercase
@@ -3553,7 +3432,7 @@ local function GetSpellNames(spellID)
return cached.base, cached.full
end
local fullName = GetSpellRecField(spellID, "name")
local fullName = C_Spell.GetSpellName(spellID)
if not fullName then return nil, nil end
local baseName = _string_gsub(fullName, _RANK_PATTERN, "")
@@ -4208,7 +4087,7 @@ function CleveRoids.ValidateUnitDebuff(unit, args)
local fallbackNameLower = _string_lower(args.name)
for sid, rec in pairs(lib.objects[guid]) do
if rec and rec.caster == "player" then
local n = GetSpellRecField and GetSpellRecField(sid, "name")
local n = C_Spell.GetSpellName(sid)
if n then
n = CleveRoids.StripRank(n)
if _string_lower(n) == fallbackNameLower then
@@ -4295,7 +4174,7 @@ function CleveRoids.ValidateUnitDebuff(unit, args)
local cleanupNameLower = _string_lower(args.name)
for sid, rec in pairs(lib.objects[guid]) do
if rec and rec.caster == "player" then
local n = GetSpellRecField and GetSpellRecField(sid, "name")
local n = C_Spell.GetSpellName(sid)
if n then
n = CleveRoids.StripRank(n)
if _string_lower(n) == cleanupNameLower then
@@ -4629,19 +4508,9 @@ function CleveRoids.ValidatePlayerDebuff(args)
end
function CleveRoids.ValidateWeaponImbue(slot, args)
-- Check if weapon has enchant via API
local hasMainEnchant, mainExpiration, mainCharges, hasOffEnchant, offExpiration, offCharges = GetWeaponEnchantInfo()
local hasEnchant, expiration, charges
if slot == "mh" then
hasEnchant = hasMainEnchant
expiration = mainExpiration
charges = mainCharges
else
hasEnchant = hasOffEnchant
expiration = offExpiration
charges = offCharges
end
-- Temp-enchant state for this slot, read via ClassicAPI (centralizes the
-- 12-tuple indexing; enchantID unused here but powers [mhenchant]).
local hasEnchant, expiration, charges = CleveRoids.ClassicAPI.GetWeaponEnchant(slot)
-- Only consider temporary enchants (with time or charges)
-- This filters out permanent enchants like Crusader, Lifestealing, etc.
@@ -4756,6 +4625,19 @@ function CleveRoids.CheckWeaponImbueByName(slot, imbueName)
return true -- No name to check
end
-- Fast path: resolve the applied temp-enchant's ID -> localized name via
-- ClassicAPI (SpellItemEnchantment.dbc) and match that directly -- exact,
-- locale-clean, no green-text tooltip heuristics. Only nameless enchants
-- (e.g. sharpening stones, which show "+N Weapon Damage" and carry no DBC
-- name) return nil here and fall through to the tooltip scan below.
local _, _, _, enchantID = CleveRoids.ClassicAPI.GetWeaponEnchant(slot)
local enchantName = enchantID and CleveRoids.ClassicAPI.GetEnchantName(enchantID)
if enchantName then
local nlower = GetLowerNormalizedName(enchantName)
local want = GetLowerNormalizedName(imbueName)
return nlower == want or string.find(nlower, want, 1, true) ~= nil
end
-- Create tooltip scanner if needed
if not CleveRoidsTooltip then
CreateFrame("GameTooltip", "CleveRoidsTooltip", nil, "GameTooltipTemplate")
@@ -4796,6 +4678,48 @@ function CleveRoids.CheckWeaponImbueByName(slot, imbueName)
return false
end
-- Does a single [mhenchant]/[ohenchant] value match the applied enchant?
-- value is a numeric enchant ID (exact) or a name (resolved from the applied
-- enchantID via ClassicAPI's SpellItemEnchantment lookup -- no tooltip scan).
-- Name match is case-insensitive, exact-or-substring (so "Deadly" matches
-- "Deadly Poison"). A parsed-arg table (from an operator form) uses its .name.
local function EnchantValueMatches(value, enchantID)
if type(value) == "table" then value = value.name end
if not value or value == "" then return false end
local wantId = tonumber(value)
if wantId then
return enchantID == wantId
end
local name = CleveRoids.ClassicAPI.GetEnchantName(enchantID)
if not name then return false end
local nlower = GetLowerNormalizedName(name)
local want = GetLowerNormalizedName(value)
return nlower == want or string.find(nlower, want, 1, true) ~= nil
end
-- Matches the temporary weapon enchant on `slot` ("mh"/"oh") by enchant ID or
-- localized name, both resolved via ClassicAPI (C_Item.GetWeaponEnchantInfo +
-- GetEnchantInfo) -- locale-proof and exact, unlike the tooltip-scanned
-- [mhimbue:Name]. value may be true/nil (any temp enchant), a single ID/name,
-- or an OR-list array; returns true if the applied enchant matches any entry.
function CleveRoids.ValidateWeaponEnchant(slot, value)
local hasEnchant, expiration, charges, enchantID = CleveRoids.ClassicAPI.GetWeaponEnchant(slot)
local hasTemp = hasEnchant and ((expiration and expiration > 0) or (charges and charges > 0))
if not hasTemp then return false end
-- Bare [mhenchant]: any temporary enchant present.
if value == nil or value == true then return true end
-- OR-list (e.g. [mhenchant:2823/Deadly_Poison]) is an array of strings.
if type(value) == "table" and not value.name and table.getn(value) > 0 then
for i = 1, table.getn(value) do
if EnchantValueMatches(value[i], enchantID) then return true end
end
return false
end
return EnchantValueMatches(value, enchantID)
end
-- TODO: Look into https://github.com/Stanzilla/WoWUIBugs/issues/47 if needed
-- PERFORMANCE: Uncached version - called by GetCachedCooldown
function CleveRoids._GetCooldownUncached(name, ignoreGCD)
@@ -5089,7 +5013,7 @@ function CleveRoids.CheckSpellCast(unit, spell)
if not CleveRoids.spell_tracking[guid] then
return false
else
if spell == GetSpellRecField(CleveRoids.spell_tracking[guid].spell_id, "name") or (spell == "") then
if spell == C_Spell.GetSpellName(CleveRoids.spell_tracking[guid].spell_id) or (spell == "") then
return true
end
return false
@@ -5177,6 +5101,33 @@ function CleveRoids.GetSpellMechanic(spellID)
return CleveRoids.ClassicAPI.GetSpellMechanicByID(spellID) or 0
end
-- School name -> spell-school mask bit (WoW SPELL_SCHOOL_MASK_*).
local LOCKOUT_SCHOOL_MASK = {
physical = 1, holy = 2, fire = 4, nature = 8, frost = 16, shadow = 32, arcane = 64,
}
-- Is `bit` set in `mask`? Shift right past `bit`, then test the low bit with
-- math.mod (the % operator is 5.1-only; 1.12 is Lua 5.0, but math.mod exists).
local function SchoolMaskHasBit(mask, bit)
if not mask or mask < bit then return false end
return math.mod(math.floor(mask / bit), 2) == 1
end
-- [locked] / [locked:school] -- player is under a spell-school interrupt lockout
-- (Counterspell / Kick / Pummel / Earth Shock), read from C_LossOfControl. This
-- is the lockout no debuff scan can detect; full silences stay on [mycc:silence].
-- school nil/true = any school kicked; a name matches that school's mask bit.
function CleveRoids.ValidateSchoolLocked(school)
local mask = CleveRoids.ClassicAPI.GetSchoolLockout()
if not mask or mask == 0 then return false end
if school == nil or school == true or school == "" then
return true
end
local bit = LOCKOUT_SCHOOL_MASK[string.lower(school)]
if not bit then return false end
return SchoolMaskHasBit(mask, bit)
end
-- Validate CC on a unit (target, focus, player, etc.)
-- Returns true if the unit has the specified CC mechanic active
function CleveRoids.ValidateUnitCC(unit, ccType)
@@ -5425,6 +5376,33 @@ local function ResolveFocusUnit(unit)
return unit
end
-- Builds a Keyword validator for a specific power slot (Enum.PowerType) read as a
-- percentage, mirroring [power]/[mypower]. condKey is the conditional name;
-- unitDefault is "player" (self) or "target" (@unit-overridable); powerType is the
-- Enum.PowerType id. Handles the multi-comparison (>50&<80) branch like [power].
local function MakeTypedPowerKeyword(condKey, unitDefault, powerType)
return function(conditionals)
return Multi(conditionals[condKey], function(args)
if type(args) ~= "table" then return false end
local unit = (unitDefault == "player") and "player" or (conditionals.target or "target")
if args.comparisons and type(args.comparisons) == "table" then
if not UnitExists(unit) then return false end
local maxPower = CleveRoids.ClassicAPI.UnitPowerMax(unit, powerType)
if not maxPower or maxPower <= 0 then return false end
local powerPercent = 100 * CleveRoids.ClassicAPI.UnitPower(unit, powerType) / maxPower
for _, comp in ipairs(args.comparisons) do
if not CleveRoids.operators[comp.operator] then return false end
if not CleveRoids.comparators[comp.operator](powerPercent, comp.amount) then return false end
end
return true
end
return CleveRoids.ValidateTypedPower(unit, powerType, args.operator, args.amount)
end, conditionals, condKey)
end
end
-- A list of Conditionals and their functions to validate them
CleveRoids.Keywords = {
exists = function(conditionals)
@@ -5608,6 +5586,23 @@ CleveRoids.Keywords = {
return not CleveRoids.ClassicAPI.IsStealthed()
end,
mounted = function(conditionals)
return CleveRoids.ClassicAPI.IsMounted()
end,
nomounted = function(conditionals)
return not CleveRoids.ClassicAPI.IsMounted()
end,
standing = function(conditionals)
return CleveRoids.ClassicAPI.GetPlayerStandState() == 0
end,
-- Any non-standing pose (sit / chair / sleep / kneel)
sitting = function(conditionals)
return CleveRoids.ClassicAPI.GetPlayerStandState() ~= 0
end,
casting = function(conditionals)
if type(conditionals.casting) ~= "table" then return CleveRoids.CheckSpellCast(conditionals.target, "") end
return Or(conditionals.casting, function (spell)
@@ -5786,6 +5781,26 @@ CleveRoids.Keywords = {
end, conditionals, "noset")
end,
-- [equipset:Name] — true if the saved equipment set "Name" is currently
-- equipped (ClassicAPI equipment manager, distinct from [set] tier pieces).
-- [equipset:Raid/PvP] = either set equipped. Names are case-sensitive; use _
-- for spaces, e.g. [equipset:My_Raid_Set].
equipset = function(conditionals)
return Multi(conditionals.equipset, function(v)
local name = (type(v) == "table") and v.name or v
return CleveRoids.ClassicAPI.IsEquipmentSetEquipped(name)
end, conditionals, "equipset")
end,
-- [noequipset:Name] — true if that equipment set is NOT currently equipped.
-- [noequipset:Raid/PvP] = neither equipped (De Morgan's).
noequipset = function(conditionals)
return NegatedMulti(conditionals.noequipset, function(v)
local name = (type(v) == "table") and v.name or v
return not CleveRoids.ClassicAPI.IsEquipmentSetEquipped(name)
end, conditionals, "noequipset")
end,
-- [inbag:Item] — true if item exists in bags or equipped
-- [inbag:Item<12] — true if bag count of Item is less than 12
-- Supports multi-value: [inbag:Item1/Item2 inbag:Item3] = (Item1 OR Item2) AND Item3
@@ -6008,14 +6023,14 @@ CleveRoids.Keywords = {
local groupVal = conditionals.group
-- Boolean form [group] - check if in any group
if groupVal == true then
return GetNumPartyMembers() > 0 or GetNumRaidMembers() > 0
return IsInGroup()
end
-- Value form [group:party] or [group:raid] or [group:party/raid]
return Multi(groupVal, function(groupType)
if groupType == "party" then
return GetNumPartyMembers() > 0
return IsInGroup()
elseif groupType == "raid" then
return GetNumRaidMembers() > 0
return IsInRaid()
end
return false
end, conditionals, "group")
@@ -6027,14 +6042,14 @@ CleveRoids.Keywords = {
local groupVal = conditionals.nogroup
-- Boolean form [nogroup] - check if not in any group
if groupVal == true then
return GetNumPartyMembers() == 0 and GetNumRaidMembers() == 0
return not IsInGroup()
end
-- Value form with De Morgan's law via NegatedMulti
return NegatedMulti(groupVal, function(groupType)
if groupType == "party" then
return GetNumPartyMembers() == 0
return not IsInGroup()
elseif groupType == "raid" then
return GetNumRaidMembers() == 0
return not IsInRaid()
end
return true
end, conditionals, "nogroup")
@@ -6254,6 +6269,17 @@ CleveRoids.Keywords = {
end, conditionals, "mypower")
end,
-- Type-specific power reads (percentage), for the SPECIFIC slot rather than the
-- unit's primary power. No prefix = target (@unit-overridable), my = player.
-- e.g. [@target,mana:<15] catches a near-OOM caster; [myenergy:>50] gates a
-- druid's cat rotation regardless of form.
mana = MakeTypedPowerKeyword("mana", "target", 0),
mymana = MakeTypedPowerKeyword("mymana", "player", 0),
rage = MakeTypedPowerKeyword("rage", "target", 1),
myrage = MakeTypedPowerKeyword("myrage", "player", 1),
energy = MakeTypedPowerKeyword("energy", "target", 3),
myenergy = MakeTypedPowerKeyword("myenergy", "player", 3),
rawpower = function(conditionals)
return Multi(conditionals.rawpower, function(args)
if type(args) ~= "table" then return false end
@@ -7216,6 +7242,26 @@ CleveRoids.Keywords = {
return not CleveRoids.ClassicAPI.IsSwimming()
end,
-- [indoors] - Player is under a WMO roof (building, cave, instance interior)
indoors = function(conditionals)
return CleveRoids.ClassicAPI.IsIndoors()
end,
-- [noindoors] - Player is NOT indoors
noindoors = function(conditionals)
return not CleveRoids.ClassicAPI.IsIndoors()
end,
-- [outdoors] - Player is outdoors (open sky / not inside a WMO interior)
outdoors = function(conditionals)
return CleveRoids.ClassicAPI.IsOutdoors()
end,
-- [nooutdoors] - Player is NOT outdoors
nooutdoors = function(conditionals)
return not CleveRoids.ClassicAPI.IsOutdoors()
end,
-- [rooted] - Player is currently rooted (Nampower v2.36+)
rooted = function(conditionals)
if not CleveRoids.NampowerAPI.features.hasPlayerIsRooted then
@@ -7588,6 +7634,24 @@ CleveRoids.Keywords = {
return not CleveRoids.ValidateWeaponImbue("oh", args)
end,
-- [mhenchant:ID] / [mhenchant:Name] - main hand has that SPECIFIC temporary
-- weapon enchant, matched by SpellItemEnchantment ID or localized name via
-- ClassicAPI (locale-proof, unlike [mhimbue:Name]'s tooltip scan). Bare
-- [mhenchant] = any temp enchant; [mhenchant:A/B] = either; [nomhenchant:X]
-- = not that enchant. Names use _ for spaces, e.g. [mhenchant:Deadly_Poison].
mhenchant = function(conditionals)
return CleveRoids.ValidateWeaponEnchant("mh", conditionals.mhenchant)
end,
nomhenchant = function(conditionals)
return not CleveRoids.ValidateWeaponEnchant("mh", conditionals.nomhenchant)
end,
ohenchant = function(conditionals)
return CleveRoids.ValidateWeaponEnchant("oh", conditionals.ohenchant)
end,
noohenchant = function(conditionals)
return not CleveRoids.ValidateWeaponEnchant("oh", conditionals.noohenchant)
end,
immune = function(conditionals)
-- Check if target is immune to the spell being cast or damage school
-- Usage: [immune] SpellName OR [immune:SpellName] OR [immune:fire]
@@ -8247,7 +8311,7 @@ CleveRoids.Keywords = {
if not UnitExists(unit) then return false end
return Or(conditionals.powertype, function(powerTypeName)
local powerType = UnitPowerType(unit)
local powerType = CleveRoids.ClassicAPI.UnitPowerType(unit)
local powerTypeLower = string.lower(powerTypeName or "")
if powerTypeLower == "mana" then
@@ -8270,7 +8334,7 @@ CleveRoids.Keywords = {
if not UnitExists(unit) then return true end
return NegatedMulti(conditionals.nopowertype, function(powerTypeName)
local powerType = UnitPowerType(unit)
local powerType = CleveRoids.ClassicAPI.UnitPowerType(unit)
local powerTypeLower = string.lower(powerTypeName or "")
if powerTypeLower == "mana" then
@@ -8355,6 +8419,33 @@ CleveRoids.Keywords = {
end, conditionals, "nomycc")
end,
-- [locked] / [locked:school] - player is under a spell-school interrupt
-- lockout (Counterspell/Kick/Pummel/Earth Shock), via ClassicAPI
-- C_LossOfControl -- the server lockout no debuff scan can see. Bare = any
-- school kicked; [locked:frost] = that school; [locked:fire/frost] = either.
-- Full silences are separate ([mycc:silence]). Player-only.
locked = function(conditionals)
local v = conditionals.locked
if v == nil or v == true or (type(v) == "table" and table.getn(v) == 0) then
return CleveRoids.ValidateSchoolLocked(nil)
end
return Or(v, function(school)
return CleveRoids.ValidateSchoolLocked(school)
end)
end,
-- [nolocked:school] - player is NOT school-locked. AND logic on negation:
-- [nolocked:fire/frost] = neither Fire nor Frost is kicked.
nolocked = function(conditionals)
local v = conditionals.nolocked
if v == nil or v == true or (type(v) == "table" and table.getn(v) == 0) then
return not CleveRoids.ValidateSchoolLocked(nil)
end
return NegatedMulti(v, function(school)
return not CleveRoids.ValidateSchoolLocked(school)
end, conditionals, "nolocked")
end,
-- ========================================================================
-- RESIST TRACKING CONDITIONALS
-- ========================================================================
@@ -9182,6 +9273,7 @@ CleveRoids.STATIC_CONDITIONALS = {
combat = true, nocombat = true, ic = true, ooc = true,
zone = true, nozone = true,
stealth = true, nostealth = true, stl = true, nostl = true,
mounted = true, nomounted = true, standing = true, sitting = true,
form = true, noform = true, stance = true, nostance = true,
equipped = true, noequipped = true, eq = true, noeq = true,
set = true, noset = true,
@@ -9189,6 +9281,7 @@ CleveRoids.STATIC_CONDITIONALS = {
mod = true, nomod = true,
keydown = true, nokeydown = true,
swimming = true, noswimming = true, swim = true, noswim = true,
indoors = true, noindoors = true, outdoors = true, nooutdoors = true,
rooted = true, norooted = true,
resting = true, noresting = true,
}
+10
View File
@@ -63,6 +63,16 @@ SlashCmdList.EQSLOT13 = CleveRoids.DoEquipTrinket1
SLASH_EQSLOT141 = "/equip14"
SlashCmdList.EQSLOT14 = CleveRoids.DoEquipTrinket2
-- Reclaim ClassicAPI's /equipset (command name EQUIP_SET) so it supports
-- conditionals. ClassicAPI registers the localized aliases against EQUIP_SET;
-- swapping the handler keeps every alias and adds the conditional engine.
if SlashCmdList.EQUIP_SET then
SlashCmdList.EQUIP_SET = CleveRoids.DoEquipSet
else
SLASH_EQUIPSET1 = "/equipset"
SlashCmdList.EQUIPSET = CleveRoids.DoEquipSet
end
SLASH_UNSHIFT1 = "/unshift"
SlashCmdList.UNSHIFT = CleveRoids.DoUnshift
+143 -133
View File
@@ -42,6 +42,9 @@ CleveRoids.spellIdCache = {}
-- PERFORMANCE: Spell name construction cache
CleveRoids.spellNameCache = {}
CleveRoids.playerGuid = UnitGUID('player')
CleveRoids.playerClass = UnitClassBase("player")
-- PERFORMANCE: Upvalues for frequently called global functions (avoid global lookups)
local GetTime = GetTime
local UnitExists = UnitExists
@@ -121,6 +124,10 @@ local BOOLEAN_CONDITIONALS = {
nocombat = true,
stealth = true,
nostealth = true,
mounted = true,
nomounted = true,
standing = true,
sitting = true,
channeled = true,
nochanneled = true,
checkchanneled = true,
@@ -140,6 +147,16 @@ local BOOLEAN_CONDITIONALS = {
nomhimbue = true,
ohimbue = true,
noohimbue = true,
mhenchant = true,
nomhenchant = true,
ohenchant = true,
noohenchant = true,
locked = true,
nolocked = true,
indoors = true,
noindoors = true,
outdoors = true,
nooutdoors = true,
group = true,
nogroup = true,
moving = true,
@@ -940,13 +957,14 @@ function CleveRoids.TestForActiveAction(actions)
-- Prefer IsSpellUsable result if available (Nampower)
actions.active.oom = (notEnoughPower == 1)
else
-- SuperWoW: UnitMana returns (current power, caster mana) for druids
local currentPower, casterMana = UnitMana("player")
-- For druids with SuperWoW, use caster mana for spell cost checks
local manaToCheck = currentPower
if CleveRoids.playerClass == "DRUID" and type(casterMana) == "number" then
manaToCheck = casterMana
-- Read caster mana directly for druids (the mana slot survives
-- shapeshift, 0 = Enum.PowerType.Mana); other classes use their
-- primary power. Replaces the SuperWoW UnitMana 2nd-return hack.
local manaToCheck
if CleveRoids.playerClass == "DRUID" then
manaToCheck = CleveRoids.ClassicAPI.UnitPower("player", 0)
else
manaToCheck = CleveRoids.ClassicAPI.UnitPower("player")
end
actions.active.oom = (manaToCheck < actions.active.spell.cost)
@@ -1569,18 +1587,9 @@ function CleveRoids.TryTargetFocus()
return true
end
-- Resolves the player's focus to a usable unit token.
-- Prefers pfUI's emulated focus (resolves to a real token like "party2"/"raid5")
-- for pfUI users, then falls back to ClassicAPI's native "focus" token (which
-- every UnitX function accepts) for everyone else.
-- Returns the resolved token, or nil when no focus is set so @focus clauses
-- silently fall through to the next macro alternative (no warning spam).
function CleveRoids.GetFocusUnitId()
if pfUI and pfUI.uf and pfUI.uf.focus and pfUI.uf.focus.label and pfUI.uf.focus.id
and UnitExists(pfUI.uf.focus.label .. pfUI.uf.focus.id) then
return pfUI.uf.focus.label .. pfUI.uf.focus.id
end
-- ClassicAPI native focus token (set via /focus or the FOCUSTARGET keybind)
if UnitExists("focus") then
return "focus"
end
@@ -2827,26 +2836,19 @@ function CleveRoids.DoTarget(msg)
addCandidate("pettarget")
if pfUI and pfUI.uf and pfUI.uf.focus and pfUI.uf.focus.label and pfUI.uf.focus.id then
local focusTok = pfUI.uf.focus.label .. pfUI.uf.focus.id
addCandidate(focusTok)
addCandidate(focusTok .. "target")
end
addCandidate("focus")
addCandidate("focustarget")
if CleveRoids.hasSuperwow then
local numChildren = WorldFrame:GetNumChildren()
local children = { WorldFrame:GetChildren() }
for i = 1, numChildren do
local frame = children[i]
if frame and frame:IsVisible() then
local success, guid = pcall(frame.GetName, frame, 1)
if success and guid and type(guid) == "string" and string.len(guid) > 0 then
if UnitExists(guid) then
addCandidate(guid)
end
end
end
-- Visible nameplates as targeting candidates, via ClassicAPI's nameplateN
-- unit tokens -- no SuperWoW WorldFrame child-walk / GUID scrape, and covers
-- default vanilla nameplates too. Slots are sparse (a removed plate leaves
-- its slot vacant until reused), so scan the full range rather than breaking
-- on the first gap; UnitExists returns false cleanly for a free/out-of-range
-- slot.
for i = 1, 40 do
local plate = "nameplate" .. i
if UnitExists(plate) then
addCandidate(plate)
end
end
@@ -3588,6 +3590,32 @@ function CleveRoids.DoEquipRing2(msg)
return false
end
-- Equip a ClassicAPI equipment set by name. msg is the resolved set name (after
-- conditional parsing). ClassicAPI's own /equipset is a bare
-- GetEquipmentSetID -> UseEquipmentSet with no conditional support; routing it
-- through DoWithConditionals adds [combat]/[mod]/@unit/etc. and ";"-separated
-- fallthrough (first set whose conditionals pass wins). Nil-guarded so a client
-- without the EquipmentSet API just no-ops instead of erroring.
local function _equipSetAction(msg)
if not msg or msg == "" then return false end
if type(C_EquipmentSet) ~= "table" then return false end
local setID = C_EquipmentSet.GetEquipmentSetID(msg)
if not setID then return false end
C_EquipmentSet.UseEquipmentSet(setID)
return true
end
function CleveRoids.DoEquipSet(msg)
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
for i = 1, table.getn(parts) do
local v = string.gsub(parts[i], "^%?", "")
if CleveRoids.DoWithConditionals(v, _equipSetAction, CleveRoids.FixEmptyTarget, false, _equipSetAction) then
return true
end
end
return false
end
function CleveRoids.DoCancelForm(msg)
local handled
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
@@ -3935,30 +3963,15 @@ function CleveRoids.OnUpdate(self)
CR.lastUpdate = time
-- Reclaim the top of the SendChatMessage hook chain if another addon has
-- displaced our #showtooltip filter (see EnsureSendChatMessageHook). Cheap
-- identity check; only re-hooks when actually displaced.
if CR.EnsureSendChatMessageHook then
CR.EnsureSendChatMessageHook()
-- LeafVillageAchievements/Legends also hook SendChatMessage and can bump ours
-- off the top of the chain (orphaning it / decorating #showtooltip). Reclaim
-- the top when they're present so #showtooltip stays filtered.
if not CR._watchChatHooks then
CR._watchChatHooks = (C_AddOns.IsAddOnLoaded("LeafVillageAchievements")
or C_AddOns.IsAddOnLoaded("LeafVillageLegends"))
end
-- Process deferred equipment index updates (for throttled UNIT_INVENTORY_CHANGED)
-- PERFORMANCE: Skip check entirely if no pending update
local pendingTime = CR.equipIndexPendingTime
if pendingTime and not UnitAffectingCombat("player") then
if (time - (CR.lastEquipIndexTime or 0)) >= 0.2 then
CR.lastEquipIndexTime = time
CR.equipIndexPendingTime = nil
CR.lastItemIndexTime = time
CR.IndexItems()
CR.Actions = {}
CR.Macros = {}
CR.IndexActionBars()
if CRM.realtime == 0 then
CR.QueueActionUpdate()
end
end
if CR._watchChatHooks then
CR.EnsureSendChatMessageHook()
end
-- PERFORMANCE: Check for expired reactive procs only if we have any
@@ -4811,8 +4824,8 @@ CleveRoids.Frame:RegisterEvent("PLAYER_ENTERING_WORLD")
CleveRoids.Frame:RegisterEvent("UPDATE_MACROS")
CleveRoids.Frame:RegisterEvent("SPELLS_CHANGED")
CleveRoids.Frame:RegisterEvent("ACTIONBAR_SLOT_CHANGED")
CleveRoids.Frame:RegisterEvent("BAG_UPDATE")
CleveRoids.Frame:RegisterEvent("UNIT_INVENTORY_CHANGED")
CleveRoids.Frame:RegisterEvent("BAG_UPDATE_DELAYED")
CleveRoids.Frame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
CleveRoids.Frame:RegisterEvent("UNIT_PET")
-- == STATE CHANGE EVENT REGISTRATION (for performance) ==
@@ -4825,6 +4838,13 @@ CleveRoids.Frame:RegisterEvent("PLAYER_REGEN_ENABLED") -- Left actual combat (n
CleveRoids.Frame:RegisterEvent("UPDATE_SHAPESHIFT_FORM")
CleveRoids.Frame:RegisterEvent("SPELL_UPDATE_COOLDOWN")
CleveRoids.Frame:RegisterEvent("PLAYER_STARTED_MOVING")
-- ClassicAPI loss-of-control (school-interrupt lockout) for [locked]/[nolocked].
-- Gated on the namespace so an older ClassicAPI without it doesn't error on an
-- unknown event.
if type(C_LossOfControl) == "table" then
CleveRoids.Frame:RegisterEvent("LOSS_OF_CONTROL_ADDED")
CleveRoids.Frame:RegisterEvent("LOSS_OF_CONTROL_UPDATE")
end
-- Use GUID events when available (v2.39+), fall back to standard per-token events
if CleveRoids.NampowerAPI.features.hasUnitGuidEvents then
CleveRoids.Frame:RegisterEvent("UNIT_AURA_GUID")
@@ -4939,7 +4959,6 @@ function CleveRoids.Frame:PLAYER_LOGIN()
if CleveRoids.disabled then return end
_, CleveRoids.playerClass = UnitClass("player")
_, CleveRoids.playerGuid = UnitExists("player")
CleveRoids.IndexSpells()
CleveRoids.IndexPetSpells()
CleveRoids.initializationTimer = GetTime() + 1.5
@@ -5028,7 +5047,7 @@ function CleveRoids.Frame:UNIT_CASTEVENT(caster,target,action,spell_id,cast_time
-- Refresh the Judgement by updating the start time
rec.start = GetTime()
local spellName = GetSpellRecField(spellID, "name")
local spellName = C_Spell.GetSpellName(spellID)
local baseName = CleveRoids.StripRank(spellName) or "Unknown"
if CleveRoids.debug then
@@ -5064,7 +5083,7 @@ function CleveRoids.Frame:UNIT_CASTEVENT(caster,target,action,spell_id,cast_time
-- Debug channel tracking
if CleveRoids.ChannelTimeDebug then
local spellName = spell_id and GetSpellRecField and GetSpellRecField(spell_id, "name") or "Unknown"
local spellName = spell_id and C_Spell.GetSpellName(spell_id) or "Unknown"
if string.find(spellName, "Arcane") then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[UNIT_CASTEVENT]|r %s: %s (ID:%s) caster=%s player=%s",
action, spellName, tostring(spell_id), tostring(caster), tostring(CleveRoids.playerGuid)))
@@ -5137,8 +5156,8 @@ function CleveRoids.Frame:UNIT_CASTEVENT(caster,target,action,spell_id,cast_time
if CleveRoids.currentSequence and caster == CleveRoids.playerGuid then
local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence)
local name = GetSpellRecField(spell_id, "name")
local rank = GetSpellRecField(spell_id, "rank")
local name = C_Spell.GetSpellName(spell_id)
local rank = C_Spell.GetSpellSubtext(spell_id)
local nameRank = (rank and rank ~= "") and (name .. "(" .. rank .. ")") or nil
local isSeqSpell = active and active.action and (
active.action == name or
@@ -5178,7 +5197,7 @@ function CleveRoids.Frame:SPELL_CAST_EVENT(success, spellId, castType, targetGui
CleveRoids.CurrentSpell.type = "channeled"
CleveRoids.CurrentSpell.castingSpellId = spellId
local spellName = GetSpellRecField and GetSpellRecField(spellId, "name")
local spellName = C_Spell.GetSpellName(spellId)
if spellName then
CleveRoids.CurrentSpell.spellName = spellName
end
@@ -5204,8 +5223,8 @@ function CleveRoids.Frame:SPELL_CAST_EVENT(success, spellId, castType, targetGui
if CleveRoids.currentSequence and success == 1 then
local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence)
if active and active.action then
local name = GetSpellRecField(spellId, "name")
local rank = GetSpellRecField(spellId, "rank")
local name = C_Spell.GetSpellName(spellId)
local rank = C_Spell.GetSpellSubtext(spellId)
local nameRank = (rank and rank ~= "") and (name .. "(" .. rank .. ")") or nil
local isSeqSpell = (active.action == name or (nameRank and active.action == nameRank))
@@ -5222,7 +5241,7 @@ function CleveRoids.Frame:SPELL_CAST_EVENT(success, spellId, castType, targetGui
if CleveRoids.currentSequence and success == 0 then
local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence)
if active and active.action then
local name = GetSpellRecField(spellId, "name")
local name = C_Spell.GetSpellName(spellId)
local isSeqSpell = (active.action == name)
if isSeqSpell then
CleveRoids.currentSequence.status = 1 -- Reset to retry
@@ -5280,8 +5299,8 @@ function CleveRoids.Frame:SPELL_START_SELF(casterGuid, targetGuid, spellId, cast
if CleveRoids.currentSequence and castTimeMs and castTimeMs > 0 then
local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence)
if active and active.action then
local name = GetSpellRecField(spellId, "name")
local rank = GetSpellRecField(spellId, "rank")
local name = C_Spell.GetSpellName(spellId)
local rank = C_Spell.GetSpellSubtext(spellId)
local nameRank = (rank and rank ~= "") and (name .. "(" .. rank .. ")") or nil
local isSeqSpell = (active.action == name or (nameRank and active.action == nameRank))
@@ -5318,7 +5337,7 @@ function CleveRoids.Frame:SPELL_FAILED_SELF(casterGuid, targetGuid, spellId, ...
if CleveRoids.currentSequence then
local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence)
if active and active.action then
local name = GetSpellRecField(spellId, "name")
local name = C_Spell.GetSpellName(spellId)
local isSeqSpell = name and (active.action == name)
if isSeqSpell then
CleveRoids.currentSequence.status = 1 -- Reset to retry
@@ -5357,7 +5376,7 @@ function CleveRoids.Frame:SPELLCAST_CHANNEL_START()
-- Update spell info
if spellId then
CleveRoids.CurrentSpell.castingSpellId = spellId
local spellName = GetSpellRecField(spellId, "name")
local spellName = C_Spell.GetSpellName(spellId)
if spellName then
CleveRoids.CurrentSpell.spellName = spellName
end
@@ -5437,7 +5456,7 @@ function CleveRoids.Frame:SPELLCAST_START()
-- Update spell info
if spellId then
CleveRoids.CurrentSpell.castingSpellId = spellId
local spellName = GetSpellRecField(spellId, "name")
local spellName = C_Spell.GetSpellName(spellId)
if spellName then
CleveRoids.CurrentSpell.spellName = spellName
end
@@ -5672,7 +5691,7 @@ function CleveRoids.Frame:ACTIONBAR_SLOT_CHANGED()
end
end
function CleveRoids.Frame:BAG_UPDATE()
function CleveRoids.Frame:BAG_UPDATE_DELAYED()
-- In combat: Skip expensive indexing but still queue icon update
-- so conditionals like [inbag] re-evaluate (they use live bag APIs)
if UnitAffectingCombat("player") then
@@ -5701,34 +5720,22 @@ function CleveRoids.Frame:BAG_UPDATE()
end
end
function CleveRoids.Frame:UNIT_INVENTORY_CHANGED()
if arg1 ~= "player" then return end
function CleveRoids.Frame:PLAYER_EQUIPMENT_CHANGED()
-- arg1 = inventory slot that changed, arg2 = hasCurrent (slot now holds an item)
local slot, hasCurrent = arg1, arg2
-- PERFORMANCE: Invalidate equipment cache for HasGearEquipped
if CleveRoids.InvalidateEquipmentCache then
CleveRoids.InvalidateEquipmentCache()
end
-- In combat: Skip ALL processing - EquipBagItem already handles cache invalidation
-- This eliminates lag from IndexEquippedItems during rapid gear swapping
-- In combat: skip the slot reindex to avoid lag during rapid gear swapping.
-- [equipped] reads live engine state (C_Item.IsEquippedItem) so it stays
-- correct regardless; the Items table self-heals on the next BAG_UPDATE.
if UnitAffectingCombat("player") then
return
end
-- Out of combat: Full indexing with throttle
local now = GetTime()
if (now - (CleveRoids.lastEquipIndexTime or 0)) < 0.2 then
CleveRoids.equipIndexPendingTime = now
return
end
CleveRoids.lastEquipIndexTime = now
CleveRoids.equipIndexPendingTime = nil
CleveRoids.lastItemIndexTime = now
CleveRoids.IndexItems()
CleveRoids.Actions = {}
CleveRoids.Macros = {}
CleveRoids.IndexActionBars()
-- Out of combat: reindex only the one slot that changed (the bag side of any
-- swap is covered by BAG_UPDATE_DELAYED). No action-bar rebuild is needed -
-- gear swaps don't change what's on the bars; QueueActionUpdate re-evaluates
-- [equipped:...] icons/conditionals.
CleveRoids.IndexEquipSlot(slot, hasCurrent)
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
@@ -5765,6 +5772,13 @@ function CleveRoids.Frame:PLAYER_FOCUS_CHANGED()
CleveRoids.QueueActionUpdate()
end
end
-- School-interrupt lockout applied/changed -> refresh [locked]/[nolocked] icons.
function CleveRoids.Frame:LOSS_OF_CONTROL_ADDED()
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
CleveRoids.Frame.LOSS_OF_CONTROL_UPDATE = CleveRoids.Frame.LOSS_OF_CONTROL_ADDED
function CleveRoids.Frame:UPDATE_SHAPESHIFT_FORM()
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
@@ -5887,11 +5901,9 @@ function CleveRoids.Frame:SPELL_QUEUE_EVENT()
queueType = eventCode,
queueTime = GetTime()
}
if GetSpellRecField then
local name = GetSpellRecField(spellId, "name")
if name then
CleveRoids.queuedSpell.spellName = name
end
local name = C_Spell.GetSpellName(spellId)
if name then
CleveRoids.queuedSpell.spellName = name
end
-- BUGFIX: Update casting state when spell is queued (for [casting] conditional)
if CleveRoids.UpdateCastingState then
@@ -5928,11 +5940,9 @@ function CleveRoids.Frame:SPELL_CAST_EVENT()
targetGuid = targetGuid,
timestamp = GetTime()
}
if GetSpellRecField then
local name = GetSpellRecField(spellId, "name")
if name then
CleveRoids.lastCastSpell.spellName = name
end
local name = C_Spell.GetSpellName(spellId)
if name then
CleveRoids.lastCastSpell.spellName = name
end
-- Track pending cast for SPELL_GO correlation (reactive ability detection)
@@ -5962,7 +5972,7 @@ function CleveRoids.Frame:SPELL_CAST_EVENT()
if cp > 0 then
CleveRoids.pendingCasts[spellId].comboPoints = cp
if CleveRoids.debug then
local castSpellName = GetSpellRecField and GetSpellRecField(spellId, "name") or "Unknown"
local castSpellName = C_Spell.GetSpellName(spellId) or "Unknown"
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ff88[SPELL_CAST_EVENT]|r Captured %d CP for %s (ID:%d)",
cp, castSpellName, spellId)
@@ -5997,27 +6007,27 @@ function CleveRoids.Frame:KEY_UP()
CleveRoids.isActionUpdateQueued = true
end
-- Filter out the #showtooltip line that Blizzard's native macro executor sends
-- to chat when a macro runs from an action button. Named (not anonymous) so we
-- can detect displacement and reclaim the top of the hook chain.
-- Base SendChatMessage captured at first hook; used to break a hook cycle.
local baseSendChatMessage = SendChatMessage
local sendingChatMessage = false
-- Swallow #showtooltip lines Blizzard's macro executor sends to chat.
local function CleveRoids_SendChatMessage(msg, ...)
if msg and string.find(msg, "^#showtooltip") then
return
end
-- Call whatever we chained over (another addon's hook, or the real function)
CleveRoids.Hooks.SendChatMessage(msg, unpack(arg))
if sendingChatMessage then
-- Re-entered via a hook cycle; go straight to base to avoid recursion.
return baseSendChatMessage(msg, unpack(arg))
end
sendingChatMessage = true
local ok, err = pcall(CleveRoids.Hooks.SendChatMessage, msg, unpack(arg))
sendingChatMessage = false
if not ok and geterrorhandler then geterrorhandler()(err) end
end
-- (Re-)assert our filter as the OUTERMOST SendChatMessage hook. This is a no-op
-- once we're already on top. It matters because some addons snapshot
-- SendChatMessage at their file-load and later install a hook that calls that
-- snapshot DIRECTLY -- if they loaded before us, their snapshot predates our
-- filter, so calling it directly orphans us and #showtooltip leaks to chat.
-- (LeafVillageAchievements does exactly this at PLAYER_ENTERING_WORLD+3s; and
-- because the fork sorts after it alphabetically, we load too late to be in its
-- snapshot -- upstream "CleveRoidMacros" sorted before it and wasn't affected.)
-- OnUpdate calls this so we reclaim the top within a frame of being displaced;
-- being outermost also guarantees we see the pristine line for the anchor match.
-- Reassert our filter as the outermost SendChatMessage hook (no-op if already on
-- top). See the OnUpdate caller for why this is needed.
function CleveRoids.EnsureSendChatMessageHook()
if SendChatMessage ~= CleveRoids_SendChatMessage then
CleveRoids.Hooks.SendChatMessage = SendChatMessage
@@ -6226,7 +6236,7 @@ SlashCmdList["CLEVEROID"] = function(msg)
CleveRoids_LearnedDurations = CleveRoids_LearnedDurations or {}
CleveRoids_LearnedDurations[spellID] = CleveRoids_LearnedDurations[spellID] or {}
CleveRoids_LearnedDurations[spellID][playerGUID] = duration
local spellName = GetSpellRecField(spellID, "name") or "Unknown"
local spellName = C_Spell.GetSpellName(spellID) or "Unknown"
CleveRoids.Print("Set " .. spellName .. " (ID:" .. spellID .. ") duration to " .. duration .. "s")
else
CleveRoids.Print("Usage: /cleveroid learn <spellID> <duration> - Manually set spell duration")
@@ -6243,7 +6253,7 @@ SlashCmdList["CLEVEROID"] = function(msg)
else
local spellID = tonumber(val)
if spellID and CleveRoids_LearnedDurations and CleveRoids_LearnedDurations[spellID] then
local spellName = GetSpellRecField(spellID, "name") or "Unknown"
local spellName = C_Spell.GetSpellName(spellID) or "Unknown"
CleveRoids_LearnedDurations[spellID] = nil
CleveRoids.Print("Forgot " .. spellName .. " (ID:" .. spellID .. ") duration")
elseif spellID then
@@ -6429,7 +6439,7 @@ SlashCmdList["CLEVEROID"] = function(msg)
CleveRoids.Print(schoolColor .. string.upper(school) .. "|r (" .. table.getn(spellIDs) .. " spells):")
for _, spellID in ipairs(spellIDs) do
local spellName = GetSpellRecField and GetSpellRecField(spellID, "name") or "Unknown"
local spellName = C_Spell.GetSpellName(spellID) or "Unknown"
CleveRoids.Print(" " .. spellName .. " (ID:" .. spellID .. ")")
end
end
@@ -6472,7 +6482,7 @@ SlashCmdList["CLEVEROID"] = function(msg)
CleveRoids.Print("No learned combo durations yet. Cast finishers and let them expire!")
else
for spellID, cpData in pairs(CleveRoids_ComboDurations) do
local spellName = GetSpellRecField(spellID, "name") or ("Spell " .. spellID)
local spellName = C_Spell.GetSpellName(spellID) or ("Spell " .. spellID)
CleveRoids.Print(spellName .. " (ID:" .. spellID .. "):")
for cp = 1, 5 do
if cpData[cp] then
@@ -6554,7 +6564,7 @@ SlashCmdList["CLEVEROID"] = function(msg)
return
end
local spellName = GetSpellRecField(spellID, "name") or ("Spell " .. spellID)
local spellName = C_Spell.GetSpellName(spellID) or ("Spell " .. spellID)
local modifier = CleveRoids.talentModifiers and CleveRoids.talentModifiers[spellID]
if not modifier then
@@ -6630,7 +6640,7 @@ SlashCmdList["CLEVEROID"] = function(msg)
for spellID, rec in pairs(lib.objects[guid]) do
if rec and rec.start and rec.duration then
local timeRemaining = rec.duration + rec.start - GetTime()
local spellName = GetSpellRecField and GetSpellRecField(spellID, "name") or "Unknown"
local spellName = C_Spell.GetSpellName(spellID) or "Unknown"
local caster = rec.caster or "unknown"
local stacks = rec.stacks or 0
if timeRemaining > 0 then
@@ -6657,7 +6667,7 @@ SlashCmdList["CLEVEROID"] = function(msg)
for i = 1, 16 do
local texture, stacks, debuffType, spellID = UnitDebuff("target", i)
if texture then
local spellName = GetSpellRecField and GetSpellRecField(spellID, "name") or "slot" .. i
local spellName = C_Spell.GetSpellName(spellID) or "slot" .. i
CleveRoids.Print(string.format(" Slot %d: [%d] %s (stacks: %d)",
i, spellID or 0, spellName, stacks or 0))
debuffCount = debuffCount + 1
@@ -6677,7 +6687,7 @@ SlashCmdList["CLEVEROID"] = function(msg)
-- Check if this might be an overflow debuff by checking libdebuff durations
local isDebuff = lib and lib.durations and lib.durations[spellID]
if isDebuff then
local spellName = GetSpellRecField and GetSpellRecField(spellID, "name") or "slot" .. i
local spellName = C_Spell.GetSpellName(spellID) or "slot" .. i
CleveRoids.Print(string.format(" Buff Slot %d (=Debuff %d): [%d] %s (stacks: %d) |cffff8800OVERFLOW|r",
i, i + 16, spellID, spellName, stacks or 0))
overflowCount = overflowCount + 1
@@ -6718,7 +6728,7 @@ SlashCmdList["CLEVEROID"] = function(msg)
-- Also check GetSpellRecField
if GetSpellRecField then
for id = 1, 30000 do
local name = GetSpellRecField(id, "name")
local name = C_Spell.GetSpellName(id)
if name and string.lower(name) == string.lower(searchName) then
local found = false
for _, existingID in ipairs(foundIDs) do
+4 -4
View File
@@ -209,8 +209,8 @@ local function InjectCustomSpells()
local count = 0
for spellID, data in pairs(CleveRoids.CustomCursiveSpells) do
-- Get texture from GetSpellRecField + GetSpellIconTexture
local name = GetSpellRecField(spellID, "name")
local rank = GetSpellRecField(spellID, "rank")
local name = C_Spell.GetSpellName(spellID)
local rank = C_Spell.GetSpellSubtext(spellID)
local texture = CleveRoids.libdebuff and CleveRoids.libdebuff:GetCachedIcon(spellID)
if texture then
-- Always update/add (in case Cursive reloaded and cleared them)
@@ -524,8 +524,8 @@ CleveRoids.HandleConsoleCommand = function(msg)
return
end
local name = GetSpellRecField(spellID, "name")
local rank = GetSpellRecField(spellID, "rank")
local name = C_Spell.GetSpellName(spellID)
local rank = C_Spell.GetSpellSubtext(spellID)
local texture = CleveRoids.libdebuff and CleveRoids.libdebuff:GetCachedIcon(spellID)
if not name then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Error:|r Spell ID " .. spellID .. " not found.")
+3 -13
View File
@@ -1391,12 +1391,6 @@ end
-- Extension Entry Points
-- ============================================================================
function Extension.OnAddonLoaded()
if arg1 == "Blizzard_MacroUI" then
InstallHooks()
end
end
function Extension.OnLoad()
-- Skip if macro checker is disabled
if CleveRoidMacros and CleveRoidMacros.macrocheck == 0 then return end
@@ -1404,13 +1398,9 @@ function Extension.OnLoad()
-- Skip if SuperMacro is loaded (detected at load time)
if SuperMacroFrame ~= nil then return end
-- Listen for macro UI loading
Extension.RegisterEvent("ADDON_LOADED", "OnAddonLoaded")
-- If MacroFrame already exists (unlikely but safe), hook immediately
if MacroFrame and MacroFrameText then
InstallHooks()
end
-- Install once Blizzard's macro UI is available (fires immediately if already
-- loaded), replacing the ADDON_LOADED listener + manual "already loaded" check.
EventUtil.ContinueOnAddOnLoaded("Blizzard_MacroUI", InstallHooks)
end
_G["CleveRoids"] = CleveRoids
+4 -15
View File
@@ -133,12 +133,6 @@ function Extension.OnMacroFrameLoad()
end
end
function Extension.OnAddonLoaded()
if arg1 == "Blizzard_MacroUI" then
Extension.OnMacroFrameLoad()
end
end
function Extension.OnLoad()
-- Schedule messages to show after UI is ready
local function ShowMessages()
@@ -178,16 +172,11 @@ function Extension.OnLoad()
end
end
-- Listen for macro UI loading
Extension.RegisterEvent("ADDON_LOADED", "OnAddonLoaded")
-- Hook the macro UI once available (fires immediately if already loaded).
EventUtil.ContinueOnAddOnLoaded("Blizzard_MacroUI", Extension.OnMacroFrameLoad)
-- Also try to hook MacroFrame_SaveMacro if it already exists
if MacroFrame_SaveMacro then
Extension.OnMacroFrameLoad()
end
-- Register PLAYER_LOGIN to show status messages
Extension.RegisterEvent("PLAYER_LOGIN", "OnPlayerLogin")
-- Status messages on login (currently disabled inside OnPlayerLogin).
EventUtil.ContinueOnPlayerLogin(Extension.OnPlayerLogin)
-- Store the message function for later
Extension.ShowMessages = ShowMessages
+4 -6
View File
@@ -6,7 +6,6 @@ local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
local Extension = CleveRoids.RegisterExtension("CT_RaidAssist")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnter()
local tempOptions = CT_RAMenu_Options["temp"]
@@ -48,15 +47,14 @@ function Extension.OnLeave()
CleveRoids.ClearMouseoverFrom("native")
end
function Extension.OnLoad()
if arg1 ~= "CT_RaidAssist" then
return
end
function Extension.OnLoad() end
function Extension.OnAddOnLoad()
if not CT_RA_MemberFrame_OnEnter then return end
Extension.Hook("CT_RA_MemberFrame_OnEnter", "OnEnter")
Extension.HookMethod(_G["GameTooltip"], "Hide", "OnLeave")
Extension.HookMethod(_G["GameTooltip"], "FadeOut", "OnLeave")
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("CT_RaidAssist", Extension.OnAddOnLoad)
+5 -4
View File
@@ -6,7 +6,6 @@ local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
local Extension = CleveRoids.RegisterExtension("CT_UnitFrames")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.SetHook(widget)
local hookedOnEnter = widget:GetScript("OnEnter")
@@ -24,8 +23,10 @@ function Extension.SetHook(widget)
end)
end
function Extension.OnLoad()
if arg1 ~= "CT_UnitFrames" or not CT_AssistFrame then
function Extension.OnLoad() end
function Extension.OnAddOnLoad()
if not CT_AssistFrame then
return
end
CleveRoids.Print("CT_UnitFrames module loaded.")
@@ -36,4 +37,4 @@ function Extension.OnLoad()
Extension.SetHook(CT_AssistFrame_Drag)
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("CT_UnitFrames", Extension.OnAddOnLoad)
+8 -17
View File
@@ -9,7 +9,6 @@ local CleveRoids = _G.CleveRoids or {}
CleveRoids.Hooks = CleveRoids.Hooks or {}
local Extension = CleveRoids.RegisterExtension("CursiveMouseover")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
local hooked = false
@@ -66,21 +65,13 @@ local function HookCursiveUI()
end
function Extension.OnLoad()
-- Try to hook when Cursive loads
if arg1 == "Cursive" then
-- Delay slightly to ensure Cursive.ui is initialized
local frame = CreateFrame("Frame")
frame:SetScript("OnUpdate", function()
if HookCursiveUI() then
this:Hide()
end
end)
end
-- Delay slightly to ensure Cursive.ui is initialized
local frame = CreateFrame("Frame")
frame:SetScript("OnUpdate", function()
if HookCursiveUI() then
this:Hide()
end
end)
end
-- Also try to hook immediately in case Cursive is already loaded
if Cursive and Cursive.ui then
HookCursiveUI()
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("Cursive", Extension.OnLoad)
+4 -6
View File
@@ -6,7 +6,6 @@ local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
local Extension = CleveRoids.RegisterExtension("DiscordUnitFrames")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnterFrame()
CleveRoids.SetMouseoverFrom("duf", this.unit)
@@ -26,11 +25,10 @@ function Extension.OnLeaveElement()
CleveRoids.ClearMouseoverFrom("native")
end
function Extension.OnLoad()
if arg1 ~= "DiscordUnitFrames" then
return
end
function Extension.OnLoad() end
function Extension.OnAddOnLoad()
if not DUF_UnitFrame_OnEnter then return end
CleveRoids.ClearHooks()
Extension.Hook("DUF_UnitFrame_OnEnter", "OnEnterFrame")
Extension.Hook("DUF_UnitFrame_OnLeave", "OnLeaveFrame")
@@ -39,4 +37,4 @@ function Extension.OnLoad()
Extension.Hook("DUF_Element_OnLeave", "OnLeaveElement")
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("DiscordUnitFrames", Extension.OnAddOnLoad)
+4 -6
View File
@@ -8,7 +8,6 @@ local CleveRoids = _G.CleveRoids or {}
CleveRoids.Hooks = CleveRoids.Hooks or {}
local Extension = CleveRoids.RegisterExtension("Grid")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnter(unit)
CleveRoids.SetMouseoverFrom("grid", unit)
@@ -19,11 +18,10 @@ function Extension.OnLeave()
CleveRoids.ClearMouseoverFrom("native")
end
function Extension.OnLoad()
if arg1 ~= "Grid" then
return
end
function Extension.OnLoad() end
function Extension.OnAddOnLoad()
if not GridFrame then return end
CleveRoids.Hooks.Grid = { CreateFrames = GridFrame.frameClass.prototype.CreateFrames}
GridFrame.frameClass.prototype.CreateFrames = CleveRoids.GrdCreateFrames
end
@@ -111,4 +109,4 @@ function CleveRoids:GrdCreateFrames()
ClickCastFrames[self.frame] = true
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("Grid", Extension.OnAddOnLoad)
+4 -4
View File
@@ -8,7 +8,6 @@ local CleveRoids = _G.CleveRoids or {}
local CreateFrames = nil
local Extension = CleveRoids.RegisterExtension("NotGrid")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnter()
CleveRoids.SetMouseoverFrom("ngrid", this.unit)
@@ -39,7 +38,9 @@ function CleveRoids:NotGrid_CreateFrames()
end
end
function Extension.OnLoad()
function Extension.OnLoad() end
function Extension.OnAddOnLoad()
-- NotGrid loads before CleveRoids, so if NotGrid is enabled, then it's global will exist.
if not NotGrid then
return
@@ -48,7 +49,6 @@ function Extension.OnLoad()
CreateFrames = NotGrid.CreateFrames
NotGrid.CreateFrames = CleveRoids.NotGrid_CreateFrames
Extension.UnregisterEvent("ADDON_LOADED", "Onload")
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("NotGrid", Extension.OnAddOnLoad)
+4 -6
View File
@@ -8,7 +8,6 @@ local CleveRoids = _G.CleveRoids or {}
CleveRoids.Hooks = CleveRoids.Hooks or {}
local Extension = CleveRoids.RegisterExtension("PerfectRaid")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnter(unit)
CleveRoids.SetMouseoverFrom("praid", unit)
@@ -19,11 +18,10 @@ function Extension.OnLeave()
CleveRoids.ClearMouseoverFrom("native")
end
function Extension.OnLoad()
if arg1 ~= "PerfectRaid" then
return
end
function Extension.OnLoad() end
function Extension.OnAddOnLoad()
if not PerfectRaid then return end
CleveRoids.Hooks.PerfectRaid = { CreateFrame = PerfectRaid.CreateFrame }
PerfectRaid.CreateFrame = CleveRoids.PerfectRaidCreateFrame
end
@@ -134,4 +132,4 @@ function CleveRoids.PerfectRaidCreateFrame(self, num)
--]]
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("PerfectRaid", Extension.OnAddOnLoad)
+1 -3
View File
@@ -4,7 +4,6 @@ local CleveRoids = _G.CleveRoids or {}
CleveRoids.Hooks = CleveRoids.Hooks or {}
local Extension = CleveRoids.RegisterExtension("ag_UnitFrames")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnter(unit)
CleveRoids.SetMouseoverFrom("aguf", unit)
@@ -23,7 +22,6 @@ function Extension.OnLoad()
CleveRoids.Hooks.ag_UnitFrames = { OnEnter = aUF.classes.aUFunit.prototype.OnEnter, OnLeave = aUF.classes.aUFunit.prototype.OnLeave}
aUF.classes.aUFunit.prototype.OnEnter = CleveRoids.aUFOnEnter
aUF.classes.aUFunit.prototype.OnLeave = CleveRoids.aUFOnLeave
Extension.UnregisterEvent("ADDON_LOADED", "Onload")
end
-- Taken from ag_UnitClass.lua
@@ -41,4 +39,4 @@ function CleveRoids:aUFOnLeave()
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("ag_UnitFrames", Extension.OnLoad)
+3
View File
@@ -8,6 +8,9 @@
- Resolving a real UnitID when .unit isn't set
- Properly hooking party group[0] (your own party slot) with a safe closure and defaulting to "player"
]]
if pfPlayer and pfPlayer.GetAttribute and pfPlayer:GetAttribute('unit') == 'player' then return end
local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
+4 -6
View File
@@ -6,7 +6,6 @@ local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
local Extension = CleveRoids.RegisterExtension("sRaidFrames")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension:OnEnter(frame)
CleveRoids.SetMouseoverFrom("sraid", frame.unit)
@@ -17,14 +16,13 @@ function Extension.OnLeave()
CleveRoids.ClearMouseoverFrom("native")
end
function Extension.OnLoad()
if arg1 ~= "sRaidFrames" then
return
end
function Extension.OnLoad() end
function Extension.OnAddOnLoad()
if not sRaidFrames then return end
Extension.HookMethod(sRaidFrames, "UnitTooltip", "OnEnter")
Extension.HookMethod(_G["GameTooltip"], "Hide", "OnLeave")
Extension.HookMethod(_G["GameTooltip"], "FadeOut", "OnLeave")
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("sRaidFrames", Extension.OnAddOnLoad)
+2 -2
View File
@@ -122,7 +122,7 @@ local function CreateIconButton(parent, index, iconTable)
GameTooltip:SetOwner(btn, "ANCHOR_BOTTOMLEFT")
local spellName
if data.spellId then
spellName = GetSpellRecField and GetSpellRecField(data.spellId, "name") or ("Spell " .. data.spellId)
spellName = C_Spell.GetSpellName(data.spellId) or ("Spell " .. data.spellId)
else
spellName = data.displayName
end
@@ -649,7 +649,7 @@ local function InjectTestTargetData()
for i = 1, table.getn(TEST_TARGET_SPELL_IDS) do
local spellId = TEST_TARGET_SPELL_IDS[i]
local dur = targetDurations[i] or 60
local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") or ("TestSpell" .. spellId)
local spellName = C_Spell.GetSpellName(spellId) or ("TestSpell" .. spellId)
if not CleveRoids.AllCasterAuraTracking[targetGuid][spellName] then
CleveRoids.AllCasterAuraTracking[targetGuid][spellName] = {}
end
+18 -44
View File
@@ -156,53 +156,27 @@ end
-- Lightweight equipment-only indexing for combat situations
-- Updates existing cache rather than rebuilding it
function CleveRoids.IndexEquippedItems()
local items = CleveRoids.Items or {}
for inventoryID = 1, 19 do
local itemID = GetInventoryItemID("player", inventoryID)
if itemID then
local name, link, _, _, itemType, itemSubType, _, _, texture = GetItemInfo(itemID)
if name then
local count = GetInventoryItemCount("player", inventoryID)
if not items[name] then
items[name] = {
inventoryID = inventoryID,
id = itemID,
name = name,
count = count,
texture = texture,
link = link,
}
items[itemID] = name
local lowerName = string.lower(name)
if lowerName ~= name then
items[lowerName] = name
end
else
-- Update existing entry with current equipment state
items[name].inventoryID = inventoryID
items[name].count = count
end
end
else
-- Slot is now empty - clear inventoryID from any item that was there
-- This is handled lazily by GetItem() fallback, so we skip expensive iteration
end
end
CleveRoids.lastGetItem = nil
CleveRoids.Items = items
end
-- PERFORMANCE: Index a single equipment slot instead of all 20
-- Use when we know exactly which slot changed (e.g., from EquipBagItem)
function CleveRoids.IndexEquipSlot(inventoryID)
-- Use when we know exactly which slot changed (e.g., from EquipBagItem or
-- PLAYER_EQUIPMENT_CHANGED). hasCurrent is the event's arg2 (does the slot now
-- hold an item); pass false to skip the API probe on a slot we know is empty.
function CleveRoids.IndexEquipSlot(inventoryID, hasCurrent)
if not inventoryID then return end
local items = CleveRoids.Items or {}
local itemID = GetInventoryItemID("player", inventoryID)
-- Clear any stale inventoryID still pointing at this slot: the item that was
-- here is now unequipped or swapped out (its real location comes from the
-- paired BAG_UPDATE rebuild). Equip changes are user-paced, so this table
-- scan is off the hot path.
for _, entry in pairs(items) do
if type(entry) == "table" and entry.inventoryID == inventoryID then
entry.inventoryID = nil
end
end
-- hasCurrent == false (arg2) => slot is now empty, nothing to add.
local itemID = hasCurrent ~= false and GetInventoryItemID("player", inventoryID)
if itemID then
local name, itemLink, _, _, itemType, itemSubType, _, _, texture = GetItemInfo(itemID)
if name then
@@ -378,9 +352,9 @@ function CleveRoids.GetActionButtonInfo(slot)
if not actionType then return end
if actionType == "spell" and id then
local rank = GetSpellRecField(id, "rank")
local rank = C_Spell.GetSpellSubtext(id)
if rank == "" then rank = nil end
return "SPELL", id, GetSpellRecField(id, "name"), rank
return "SPELL", id, C_Spell.GetSpellName(id), rank
elseif actionType == "item" and id then
local item = CleveRoids.GetItem(id)
return "ITEM", id, (item and item.name)
+4
View File
@@ -651,6 +651,8 @@ local function validateConditional(conditional, args, action)
combo = true,
hp = true, myhp = true, rawhp = true, myrawhp = true,
power = true, mypower = true, rawpower = true, myrawpower = true,
mana = true, mymana = true, rage = true, myrage = true,
energy = true, myenergy = true,
hplost = true, myhplost = true,
powerlost = true, mypowerlost = true,
stat = true,
@@ -678,6 +680,8 @@ local function validateConditional(conditional, args, action)
-- Check operator syntax for numeric comparisons
if args and type(args) == "string" then
local hasHpOrPower = safeStringFind(baseCond, "hp") or safeStringFind(baseCond, "power") or
safeStringFind(baseCond, "mana") or safeStringFind(baseCond, "energy") or
safeStringFind(baseCond, "rage") or
safeStringFind(baseCond, "combo") or baseCond == "stat"
if hasHpOrPower then
local hasOperator = safeStringFind(args, "[<>=~]+")
+2 -41
View File
@@ -71,7 +71,6 @@
Spell Miss Events (v2.31+):
- SPELL_MISS_SELF / SPELL_MISS_OTHER - Spell miss/resist/immune/dodge/etc.
- GetSpellPower([mode]) - Player mod damage done for all 7 schools
Aura Event State Parameter (v2.32+):
- Buff/debuff events include 7th `state` parameter (0=added, 1=removed, 2=modified)
@@ -364,9 +363,8 @@ API.VERSION_REQUIREMENTS = {
["AuraDurationEvents"] = { 2, 30, 0 },
["GetPlayerAuraDuration"] = { 2, 30, 0, "GetPlayerAuraDuration" },
-- v2.31+ - Spell miss events and spell power query
-- v2.31+ - Spell miss events
["SpellMissEvents"] = { 2, 31, 0 }, -- SPELL_MISS_SELF/OTHER events
["GetSpellPower"] = { 2, 31, 0, "GetSpellPower" },
-- v2.32+ - Aura event state parameter and stack removal fix
["AuraEventState"] = { 2, 32, 0 },
@@ -575,9 +573,8 @@ local function InitializeFeatures()
f.hasAuraDurationEvents = API.HasFeature("AuraDurationEvents")
f.hasGetPlayerAuraDuration = API.HasFeature("GetPlayerAuraDuration")
-- v2.31+ Spell miss events and spell power
-- v2.31+ Spell miss events
f.hasSpellMissEvents = API.HasFeature("SpellMissEvents")
f.hasGetSpellPower = API.HasFeature("GetSpellPower")
-- v2.32+ Aura event state parameter
f.hasAuraEventState = API.HasFeature("AuraEventState")
@@ -3387,15 +3384,6 @@ API.MISS_INFO = {
-- SPELL POWER QUERY (v2.31+)
--------------------------------------------------------------------------------
-- Get spell power for all 7 damage schools (v2.31+)
-- mode: optional mode parameter passed to GetSpellPower
-- Returns: physical, holy, fire, nature, frost, shadow, arcane (or nil if unavailable)
function API.GetSpellPower(mode)
if not API.features.hasGetSpellPower or not _G.GetSpellPower then
return nil, nil, nil, nil, nil, nil, nil
end
return _G.GetSpellPower(mode)
end
-- Get duration of a spell in milliseconds (v2.38+)
-- For channeling spells: returns the channel duration.
@@ -3504,32 +3492,5 @@ function API.GetUnitMaxHealth(unitToken)
return UnitHealthMax(unitToken)
end
-- powerType: nil=current, 0=mana, 1=rage, 2=focus, 3=energy
-- GetUnitField uses power1-power4 fields
local POWER_FIELDS = { [0] = "power1", [1] = "power2", [2] = "power3", [3] = "power4" }
local MAX_POWER_FIELDS = { [0] = "maxPower1", [1] = "maxPower2", [2] = "maxPower3", [3] = "maxPower4" }
function API.GetUnitPower(unitToken, powerType)
if API.features.hasGetUnitField and GetUnitField and powerType then
local field = POWER_FIELDS[powerType]
if field then
local val = GetUnitField(unitToken, field)
if val then return val end
end
end
return UnitMana(unitToken)
end
function API.GetUnitMaxPower(unitToken, powerType)
if API.features.hasGetUnitField and GetUnitField and powerType then
local field = MAX_POWER_FIELDS[powerType]
if field then
local val = GetUnitField(unitToken, field)
if val then return val end
end
end
return UnitManaMax(unitToken)
end
-- Expose API globally for other addons
_G.CleveRoidsNampowerAPI = API
+180 -214
View File
File diff suppressed because it is too large Load Diff
+9 -10
View File
@@ -49,16 +49,15 @@ API references are line numbers into `C:\Git\ClassicAPI\docs\API.md`.
- class-aware `[dispellable]` (only types this character can actually remove).
- optional name filtering on the type keywords (e.g. `[magic:Polymorph]`).
### 2. `C_Item.GetWeaponEnchantInfo()` — temp-enchant IDs
- **API:** `API.md:7159` — returns 12-tuple including `enchantID` for main/off/ranged.
- **Unlocks:** detect *which* temp enchant (poison/oil/sharpening stone) is
applied to a weapon, not just that one exists.
- **Naming:** `[poison]` is now taken by the target dispel-type conditional
(slice 1 above). Use weapon-specific keywords for this — e.g.
`[mhenchant:<id>]` / `[ohenchant:<id>]` (or `[mhpoison:<id>]`/`[ohpoison:<id>]`),
not a bare `[poison]`.
- Vanilla's global only reports presence; this is a genuinely new capability
(rogue/shaman/enhance).
### 2. ~~`C_Item.GetWeaponEnchantInfo()` — temp-enchant IDs~~ — DONE
- Shipped as `[mhenchant]` / `[ohenchant]` (+ `no` variants). Matches the applied
temp enchant by SpellItemEnchantment ID *or* localized name — the name path
resolves via `C_Item.GetEnchantInfo(id).name` (the ID→name table this doc
assumed we lacked), so no tooltip scan. Wrappers `ClassicAPI.GetWeaponEnchant`
/ `GetEnchantName`; `ValidateWeaponImbue` now reads through the former.
- Bare = any temp enchant; OR-lists supported (`[mhenchant:2823/Deadly_Poison]`).
- Follow-up (optional): route `[mhimbue:Name]`'s match through `GetEnchantName`
too, retiring the green-text tooltip scan in `CheckWeaponImbueByName`.
### 3. `GetUnitSpeed(unit)` + `IsFalling()` / `IsSwimming()`
- **API:** `API.md:8312`, `API.md:7629`.