42 Commits

Author SHA1 Message Date
Brues cf47d8caba Use SetSpellByID for tooltip 2026-08-06 00:33:12 -05:00
Brues 71f8f74237 Support #showtooltip spell:<id> and item:<id>
GetSpell/GetItem now resolve explicit spell:/item: ID forms. spell:<id>
prefers the player's spellbook entry via FindSpellBookSlotByID (per-rank
and pet aware) for full cost/cooldown/usability, falling back to an
id-only entry rendered via SetSpellByID for spells not in the book.
item:<id> reuses the existing numeric lookup for location-aware tooltips.

Guard the id-only path against nil spellSlot/cost in TestForActiveAction
and GetActionCooldown so display-only spell references don't crash.

Remove the dead, no-op GetSpellSlotByID stub.
2026-08-06 00:15:19 -05:00
Brues 1a98a05b7e Remove redundant local 'i' declarations
Delete unnecessary local 'i' declarations in Extensions/Mouseover/pfUI.lua (ResolvePfUnit, RegisterPartyScripts, RegisterRaidScripts, RegisterRaidMarkScripts). The for-loop headers already provide a local loop variable, so the explicit locals were redundant and could shadow variables. No functional change.
2026-08-02 15:18:48 -05:00
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 1013 additions and 872 deletions
+181 -8
View File
@@ -52,13 +52,13 @@ end
-- C_UnitAuras -- C_UnitAuras
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Scan a unit's auras (indexFn = C_UnitAuras.GetBuffDataByIndex or -- Scan one aura range of `unit` (filter = "HELPFUL" or "HARMFUL") for an aura
-- GetDebuffDataByIndex) for one matching the dispel type. Vanilla descriptors -- matching the dispel type. The filtered index self-terminates at the end of the
-- hold 32 helpful / 16 harmful slots; cap as a backstop. -- range (nil); 48 is a backstop over vanilla's 32 helpful / 16 harmful slots.
local function scanDispel(indexFn, unit, dispelType, wantAny) local function scanDispel(unit, filter, dispelType, wantAny)
local i = 1 local i = 1
while i <= 48 do while i <= 48 do
local data = indexFn(unit, i) local data = C_UnitAuras.GetAuraDataByIndex(unit, i, filter)
if not data then return false end if not data then return false end
local dn = data.dispelName local dn = data.dispelName
if dn and dn ~= "" and (wantAny or dn == dispelType) then if dn and dn ~= "" and (wantAny or dn == dispelType) then
@@ -76,10 +76,29 @@ end
-- false -> scan debuffs (defensive cleanse, default) -- false -> scan debuffs (defensive cleanse, default)
function API.UnitHasDispelType(unit, dispelType, helpful) function API.UnitHasDispelType(unit, dispelType, helpful)
if not unit or not UnitExists(unit) then return false end if not unit or not UnitExists(unit) then return false end
local indexFn = helpful and C_UnitAuras.GetBuffDataByIndex local filter = helpful and "HELPFUL" or "HARMFUL"
or C_UnitAuras.GetDebuffDataByIndex
local wantAny = (dispelType == nil or dispelType == "any") 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 end
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@@ -155,6 +174,76 @@ function API.GetItemSetInfo(setID)
return C_Item.GetItemSetInfo(setID) return C_Item.GetItemSetInfo(setID)
end 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 -- Spell
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@@ -168,6 +257,67 @@ function API.GetSpellMechanicByID(spellID)
return C_Spell.GetSpellMechanicByID(spellID) return C_Spell.GetSpellMechanicByID(spellID)
end 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 -- State
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@@ -182,6 +332,29 @@ function API.IsSwimming()
return IsSwimming() and true or false return IsSwimming() and true or false
end 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 -- Cursor
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
+40 -67
View File
@@ -32,6 +32,35 @@ CleveRoids.lastRakeCast = CleveRoids.lastRakeCast or {
timestamp = 0 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 -- Define spells that scale with combo points by SPELL ID and their duration formulas
-- Duration = base + (combo_points - 1) * increment -- Duration = base + (combo_points - 1) * increment
CleveRoids.ComboScalingSpellsByID = { CleveRoids.ComboScalingSpellsByID = {
@@ -70,32 +99,9 @@ CleveRoids.FerociousBiteSpellIDs = {
[31018] = true, -- Rank 6 [31018] = true, -- Rank 6
} }
-- Rip spell IDs (for Carnage talent - refreshed when Carnage procs) -- Rip / Rake families (for Carnage talent). Seeded by Rank 1; matches every rank.
CleveRoids.RipSpellIDs = { CleveRoids.RipSpellIDs = RankSet(1079)
[1079] = true, -- Rank 1 CleveRoids.RakeSpellIDs = RankSet(1822)
[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)
}
-- Combined table for all bleed spells that need immunity detection -- Combined table for all bleed spells that need immunity detection
-- Used when checking if a cast bleed failed to apply (indicates bleed immunity) -- 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 -- When Molten Blast hits, it refreshes Flame Shock duration on the target
-- Detection: Monitor combat log for Molten Blast damage, then refresh Flame Shock -- Detection: Monitor combat log for Molten Blast damage, then refresh Flame Shock
-- ============================================================================= -- =============================================================================
CleveRoids.MoltenBlastSpellIDs = { -- TWoW custom; seed resolves on the TWoW client (nil/never-matches on stock,
[36916] = true, -- Rank 1 -- where Molten Blast can't be cast anyway).
[36917] = true, -- Rank 2 CleveRoids.MoltenBlastSpellIDs = RankSet(36916)
[36918] = true, -- Rank 3
[36919] = true, -- Rank 4
[36920] = true, -- Rank 5
[36921] = true, -- Rank 6
}
CleveRoids.FlameShockSpellIDs = { CleveRoids.FlameShockSpellIDs = {
[8050] = true, -- Rank 1 [8050] = true, -- Rank 1
@@ -154,12 +155,7 @@ CleveRoids.FlameShockSpellIDs = {
-- WARLOCK: Conflagrate → Immolate Duration Reduction -- WARLOCK: Conflagrate → Immolate Duration Reduction
-- When Conflagrate is cast, it reduces Immolate duration by 3 seconds -- When Conflagrate is cast, it reduces Immolate duration by 3 seconds
-- ============================================================================= -- =============================================================================
CleveRoids.ConflagrateSpellIDs = { CleveRoids.ConflagrateSpellIDs = RankSet(17962)
[17962] = true, -- Rank 1
[18930] = true, -- Rank 2
[18931] = true, -- Rank 3
[18932] = true, -- Rank 4
}
CleveRoids.ImmolateSpellIDs = { CleveRoids.ImmolateSpellIDs = {
[348] = true, -- Rank 1 [348] = true, -- Rank 1
@@ -177,11 +173,8 @@ CleveRoids.ImmolateSpellIDs = {
-- Channeled spell that accelerates DoT tick rate by 30% while channeling -- Channeled spell that accelerates DoT tick rate by 30% while channeling
-- Complex tracking: debuff expires 30% faster while Dark Harvest is active -- Complex tracking: debuff expires 30% faster while Dark Harvest is active
-- ============================================================================= -- =============================================================================
CleveRoids.DarkHarvestSpellIDs = { -- TWoW custom (see MoltenBlast note).
[52550] = true, -- Rank 1 CleveRoids.DarkHarvestSpellIDs = RankSet(52550)
[52551] = true, -- Rank 2
[52552] = true, -- Rank 3
}
-- ============================================================================= -- =============================================================================
-- DRUID: Rake Debuff Cap Boss Whitelist -- DRUID: Rake Debuff Cap Boss Whitelist
@@ -489,7 +482,7 @@ function CleveRoids.TrackComboPointCastByID(spellID, targetGUID)
end end
else else
-- Second, check if name-based tracking has recent data for this spell -- 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 if spellName then
-- Remove rank info for comparison -- Remove rank info for comparison
local baseName = CleveRoids.StripRank(spellName) local baseName = CleveRoids.StripRank(spellName)
@@ -548,26 +541,6 @@ function CleveRoids.TrackComboPointCastByID(spellID, targetGUID)
return duration return duration
end 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 -- Utility function to display current combo tracking info
function CleveRoids.ShowComboTracking() function CleveRoids.ShowComboTracking()
CleveRoids.Print("=== Combo Point Tracking ===") CleveRoids.Print("=== Combo Point Tracking ===")
@@ -683,7 +656,7 @@ if _G.UseAction then
local spellName = nil local spellName = nil
if actionType == "SPELL" and actionID then if actionType == "SPELL" and actionID then
spellName = GetSpellRecField(actionID, "name") spellName = C_Spell.GetSpellName(actionID)
end end
if currentCP and currentCP > 0 then 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 CleveRoids = _G.CleveRoids or {}
local Extension = CleveRoids.RegisterExtension("Compatibility_pfUI") local Extension = CleveRoids.RegisterExtension("Compatibility_pfUI")
Extension.RegisterEvent("ADDON_LOADED", "ADDON_LOADED")
Extension.RegisterEvent("PLAYER_LOGIN", "PLAYER_LOGIN")
Extension.Debug = false 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 -- Track pfUI state
Extension.pfUILoaded = false Extension.pfUILoaded = false
@@ -99,7 +100,7 @@ local function GetCarnageOverride(effect)
end end
for spellID, override in pairs(CleveRoids.carnageDurationOverrides) do for spellID, override in pairs(CleveRoids.carnageDurationOverrides) do
local spellName = GetSpellRecField(spellID, "name") local spellName = C_Spell.GetSpellName(spellID)
if spellName then if spellName then
local baseName = CleveRoids.StripRank(spellName) local baseName = CleveRoids.StripRank(spellName)
if baseName == effect and override.timestamp and (GetTime() - override.timestamp) < 5 then 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 for spellID, rec in pairs(CleveRoids.libdebuff.objects[unitGUID]) do
if rec and rec.start and rec.duration then if rec and rec.start and rec.duration then
-- Get spell name for this ID -- Get spell name for this ID
local spellName = GetSpellRecField(spellID, "name") local spellName = C_Spell.GetSpellName(spellID)
if spellName then if spellName then
local baseName = CleveRoids.StripRank(spellName) local baseName = CleveRoids.StripRank(spellName)
if baseName == effect then if baseName == effect then
@@ -380,7 +381,7 @@ function Extension.SyncComboDurationToPfUI(guid, spellID, duration)
end end
-- Get spell name from spell ID -- Get spell name from spell ID
local spellName = GetSpellRecField(spellID, "name") local spellName = C_Spell.GetSpellName(spellID)
if not spellName then if not spellName then
if CleveRoids.debug then if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[pfUI Sync]|r Could not find spell name for ID " .. spellID) 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" SLASH_PFUICD1 = "/pfuicd"
end end
function Extension.ADDON_LOADED() -- Fires once pfUI has loaded (immediately if it loaded before us, via EventUtil).
-- Check if pfUI just loaded AND the global actually exists function Extension.OnPfUILoaded()
-- (another addon could be named "pfUI" without being the real UI framework) -- Guard: only the real pfUI framework sets this global (another addon could be
if arg1 == "pfUI" and pfUI then -- named "pfUI" without being the UI framework).
Extension.pfUILoaded = true if not pfUI then return end
-- pfUI modules load after ADDON_LOADED, so schedule a check Extension.pfUILoaded = true
if CleveRoids.ScheduleTimer then -- pfUI's submodules initialize after its ADDON_LOADED, so defer the setup.
CleveRoids.ScheduleTimer(function() if CleveRoids.ScheduleTimer then
Extension.SetupCompatibility() CleveRoids.ScheduleTimer(function()
end, 0.5) Extension.SetupCompatibility()
end end, 0.5)
end end
end end
function Extension.PLAYER_LOGIN() function Extension.OnPlayerLogin()
-- If pfUI loaded before SCRM (alphabetical order), ADDON_LOADED for pfUI was missed. -- Ensure lib.objects is linked correctly (InitPfUIIntegration is idempotent).
-- Re-run InitPfUIIntegration here to ensure lib.objects is linked correctly.
if pfUI and not CleveRoids.hasPfUI76 then if pfUI and not CleveRoids.hasPfUI76 then
local lib = CleveRoids.libdebuff local lib = CleveRoids.libdebuff
if lib and lib.InitPfUIIntegration then if lib and lib.InitPfUIIntegration then
@@ -797,4 +797,10 @@ if not CleveRoids.ScheduleTimer then
end end
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 _G["CleveRoids"] = CleveRoids
+314 -221
View File
@@ -55,15 +55,14 @@ function CleveRoids.GetCachedPlayerHealthPercent()
end end
function CleveRoids.GetCachedPlayerPowerPercent() function CleveRoids.GetCachedPlayerPowerPercent()
local API = CleveRoids.NampowerAPI local API = CleveRoids.ClassicAPI
local power = API and API.GetUnitPower and API.GetUnitPower("player") or UnitMana("player") local power = API.UnitPower("player")
local max = API and API.GetUnitMaxPower and API.GetUnitMaxPower("player") or UnitManaMax("player") local max = API.UnitPowerMax("player")
return max > 0 and (100 * power / max) or 0 return max > 0 and (100 * power / max) or 0
end end
function CleveRoids.GetCachedPlayerPower() function CleveRoids.GetCachedPlayerPower()
local API = CleveRoids.NampowerAPI return CleveRoids.ClassicAPI.UnitPower("player")
return API and API.GetUnitPower and API.GetUnitPower("player") or UnitMana("player")
end end
function CleveRoids.GetCachedTargetHealthPercent() function CleveRoids.GetCachedTargetHealthPercent()
@@ -105,7 +104,7 @@ local function BuildSpellNameCache()
if lib.personalDebuffs then if lib.personalDebuffs then
for sid, _ in pairs(lib.personalDebuffs) do for sid, _ in pairs(lib.personalDebuffs) do
local name = GetSpellRecField(sid, "name") local name = C_Spell.GetSpellName(sid)
if name then if name then
name = CleveRoids.StripRank(name) name = CleveRoids.StripRank(name)
if not _spellNameToIDs[name] then if not _spellNameToIDs[name] then
@@ -118,7 +117,7 @@ local function BuildSpellNameCache()
if lib.sharedDebuffs then if lib.sharedDebuffs then
for sid, _ in pairs(lib.sharedDebuffs) do for sid, _ in pairs(lib.sharedDebuffs) do
local name = GetSpellRecField(sid, "name") local name = C_Spell.GetSpellName(sid)
if name then if name then
name = CleveRoids.StripRank(name) name = CleveRoids.StripRank(name)
if not _spellNameToIDs[name] then if not _spellNameToIDs[name] then
@@ -163,7 +162,7 @@ local function GetSpellIDForRank(baseName, rankNum)
if not matchIDs then return nil end if not matchIDs then return nil end
local targetRank = "Rank " .. rankNum local targetRank = "Rank " .. rankNum
for _, sid in ipairs(matchIDs) do 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 if rank and rank == targetRank then
return sid return sid
end end
@@ -198,102 +197,6 @@ local function IsSharedDebuffByIdOrName(lib, spellID, debuffName)
return false return false
end 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 -- PERFORMANCE: Unified item location lookup using CleveRoids.Items cache
-- Returns: { type="inventory"|"bag", inventoryID=N } or { type="bag", bag=N, slot=N } -- 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, 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, 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, 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, healing = function() return CleveRoids.ClassicAPI.GetSpellBonusHealing() or 0 end,
healingpower = function() local _, h = CleveRoids.NampowerAPI.GetSpellPower(); return h or 0 end, healingpower = function() return CleveRoids.ClassicAPI.GetSpellBonusHealing() or 0 end,
-- Bonus Spell Damage by School (Nampower v2.31+ GetSpellPower) -- Bonus Spell Damage by School (ClassicAPI GetSpellBonusDamage)
-- GetSpellPower() returns: physical, holy, fire, nature, frost, shadow, arcane -- school: 1=Physical, 2=Holy, 3=Fire, 4=Nature, 5=Frost, 6=Shadow, 7=Arcane
arcane_power = function() return select(7, CleveRoids.NampowerAPI.GetSpellPower()) or 0 end, arcane_power = function() return CleveRoids.ClassicAPI.GetSpellBonusDamage(7) or 0 end,
fire_power = function() return select(3, CleveRoids.NampowerAPI.GetSpellPower()) or 0 end, fire_power = function() return CleveRoids.ClassicAPI.GetSpellBonusDamage(3) or 0 end,
frost_power = function() return select(5, CleveRoids.NampowerAPI.GetSpellPower()) or 0 end, frost_power = function() return CleveRoids.ClassicAPI.GetSpellBonusDamage(5) or 0 end,
nature_power = function() return select(4, CleveRoids.NampowerAPI.GetSpellPower()) or 0 end, nature_power = function() return CleveRoids.ClassicAPI.GetSpellBonusDamage(4) or 0 end,
shadow_power = function() return select(6, CleveRoids.NampowerAPI.GetSpellPower()) or 0 end, shadow_power = function() return CleveRoids.ClassicAPI.GetSpellBonusDamage(6) or 0 end,
-- Highest spell power across all schools -- Highest spell power across all schools
spell_power = function() spell_power = function()
local p, h, fi, n, fr, s, a = CleveRoids.NampowerAPI.GetSpellPower() local API = CleveRoids.ClassicAPI
if p then local best = 0
return math.max(p, h, fi, n, fr, s, a) for s = 1, 7 do
local v = API.GetSpellBonusDamage(s) or 0
if v > best then best = v end
end end
return 0 return best
end, end,
-- Defensive Stats -- Defensive Stats
@@ -916,7 +821,7 @@ function CleveRoids.GetAllCasterAuraTimeRemaining(targetGuid, spellId)
local targetData, isPfUI = CleveRoids.GetAuraTrackingData(targetGuid) local targetData, isPfUI = CleveRoids.GetAuraTrackingData(targetGuid)
if not targetData then return nil end 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 if not spellName then return nil end
local casters = targetData[spellName] local casters = targetData[spellName]
@@ -960,7 +865,7 @@ function CleveRoids.FindAllCasterAuraByName(targetGuid, searchName)
-- Resolve spell ID to name for direct lookup -- Resolve spell ID to name for direct lookup
local searchID = tonumber(searchName) local searchID = tonumber(searchName)
if searchID then if searchID then
local resolvedName = GetSpellRecField and GetSpellRecField(searchID, "name") local resolvedName = C_Spell.GetSpellName(searchID)
if not resolvedName then return nil, nil end if not resolvedName then return nil, nil end
searchName = resolvedName searchName = resolvedName
end end
@@ -1081,7 +986,7 @@ local function OnAutoAttackOther(attackerGuid, targetGuid, totalDamage, hitInfo,
rec.start = GetTime() rec.start = GetTime()
if CleveRoids.debug then 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" local baseName = CleveRoids.StripRank(spellName) or "Unknown"
DEFAULT_CHAT_FRAME:AddMessage( DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ffaa[Judgement Refresh]|r Refreshed %s (ID:%d) on melee hit - new duration: %ds", 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) -- Sync to pfUI if loaded (pre-7.6 only)
if not CleveRoids.hasPfUI76 and pfUI and pfUI.api and pfUI.api.libdebuff then 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 baseName = CleveRoids.StripRank(spellName)
local targetName = (lib.guidToName and lib.guidToName[normalizedTarget]) or UnitName("target") local targetName = (lib.guidToName and lib.guidToName[normalizedTarget]) or UnitName("target")
local targetLevel = UnitLevel("target") or 0 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) -- Use the isBuffNotDebuff result determined above (avoids redundant slot scanning)
local lib = CleveRoids.libdebuff local lib = CleveRoids.libdebuff
if isBuffNotDebuff and spellId and durationMs and durationMs > 0 and lib and not lib.hasPfUIEnhanced then 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 if spellName then
local playerGuid = CleveRoids.GetGUID("player") local playerGuid = CleveRoids.GetGUID("player")
if playerGuid then 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 -- When pfUI enhanced is active, pfUI writes to pfUI.libdebuff_all_auras with
-- full downrank protection — we read from that table via GetAuraTrackingData(). -- full downrank protection — we read from that table via GetAuraTrackingData().
if spellId and durationMs and durationMs > 0 then 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 if spellName and not CleveRoids.hasPfUI76 then
CleveRoids._allCasterAuraDirty = true CleveRoids._allCasterAuraDirty = true
if not CleveRoids.AllCasterAuraTracking[targetGuid] then 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) -- (AURA_CAST_ON_OTHER fires for both buffs and debuffs; BUFF_ADDED_OTHER confirms buff)
local lib = CleveRoids.libdebuff local lib = CleveRoids.libdebuff
if lib and not lib.hasPfUIEnhanced then if lib and not lib.hasPfUIEnhanced then
local spellNameForPending = GetSpellRecField and GetSpellRecField(spellId, "name") local spellNameForPending = C_Spell.GetSpellName(spellId)
if spellNameForPending then if spellNameForPending then
local normTargetGuid = CleveRoids.NormalizeGUID(targetGuid) local normTargetGuid = CleveRoids.NormalizeGUID(targetGuid)
if normTargetGuid then if normTargetGuid then
@@ -1472,7 +1377,7 @@ autoAttackFrame:SetScript("OnEvent", function()
if spellId and spellId > 0 and durationMs and durationMs > 0 then if spellId and spellId > 0 and durationMs and durationMs > 0 then
local playerGUID = CleveRoids.GetGUID("player") 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 if playerGUID and durSpellName and not CleveRoids.hasPfUI76 then
CleveRoids._allCasterAuraDirty = true CleveRoids._allCasterAuraDirty = true
if not CleveRoids.AllCasterAuraTracking[playerGUID] then if not CleveRoids.AllCasterAuraTracking[playerGUID] then
@@ -1524,7 +1429,7 @@ autoAttackFrame:SetScript("OnEvent", function()
local state = arg7 local state = arg7
if state == 2 then return end -- Stack change, not full removal if state == 2 then return end -- Stack change, not full removal
if guid and spellId and CleveRoids.AllCasterAuraTracking[guid] then 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 if removedName and CleveRoids.AllCasterAuraTracking[guid][removedName] then
CleveRoids.AllCasterAuraTracking[guid][removedName] = nil CleveRoids.AllCasterAuraTracking[guid][removedName] = nil
if not next(CleveRoids.AllCasterAuraTracking[guid]) then if not next(CleveRoids.AllCasterAuraTracking[guid]) then
@@ -1880,7 +1785,7 @@ local function IsPendingDebuffCast(spellName, targetUnit)
if arr then if arr then
for _, pending in pairs(arr) do for _, pending in pairs(arr) do
if pending and pending.targetGUID == targetGuid and pending.spellID then 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 if pendingName then
local normalizedPending = NormalizeSpellNameForComparison(pendingName) local normalizedPending = NormalizeSpellNameForComparison(pendingName)
if normalizedPending == normalizedCheck then if normalizedPending == normalizedCheck then
@@ -2018,7 +1923,7 @@ function CleveRoids.CancelAura(auraName)
if aura_ix == -1 then break end if aura_ix == -1 then break end
local bid = GetPlayerBuffID(aura_ix) local bid = GetPlayerBuffID(aura_ix)
bid = (bid < -1) and (bid + 65536) or bid 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) C_Spell.CancelSpellByID(bid)
return true return true
end end
@@ -2031,7 +1936,7 @@ function CleveRoids.CancelAura(auraName)
for slot = 0, 31 do for slot = 0, 31 do
local spellId = _G.GetPlayerAuraDuration(slot) local spellId = _G.GetPlayerAuraDuration(slot)
if spellId and spellId > 0 then 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 if name and string.lower(name) == auraName then
C_Spell.CancelSpellByID(spellId) C_Spell.CancelSpellByID(spellId)
return true return true
@@ -2047,7 +1952,7 @@ function CleveRoids.CancelAura(auraName)
if entry.durationSec and entry.durationSec > 0 and elapsed > entry.durationSec then if entry.durationSec and entry.durationSec > 0 and elapsed > entry.durationSec then
CleveRoids.OverflowBuffs[spellId] = nil CleveRoids.OverflowBuffs[spellId] = nil
else else
local name = GetSpellRecField(spellId, "name") local name = C_Spell.GetSpellName(spellId)
if name and string.lower(name) == auraName then if name and string.lower(name) == auraName then
C_Spell.CancelSpellByID(spellId) C_Spell.CancelSpellByID(spellId)
CleveRoids.OverflowBuffs[spellId] = nil CleveRoids.OverflowBuffs[spellId] = nil
@@ -2059,26 +1964,12 @@ function CleveRoids.CancelAura(auraName)
return false return false
end 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) function CleveRoids.HasGearEquipped(gearId)
if not gearId then return false end return (gearId and C_Item.IsEquippedItem(gearId)) or false
-- 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
end end
@@ -2952,11 +2843,8 @@ function CleveRoids.CountEnemiesMatching(checkFunc)
tryUnit("targettarget") tryUnit("targettarget")
tryUnit("targettargettarget") tryUnit("targettargettarget")
tryUnit("pettarget") tryUnit("pettarget")
if pfUI and pfUI.uf and pfUI.uf.focus and pfUI.uf.focus.label and pfUI.uf.focus.id then tryUnit("focus")
local focusUnit = pfUI.uf.focus.label .. pfUI.uf.focus.id tryUnit("focustarget")
tryUnit(focusUnit)
tryUnit(focusUnit .. "target")
end
for i = 1, 4 do for i = 1, 4 do
tryUnit("party" .. i .. "target") tryUnit("party" .. i .. "target")
end end
@@ -2966,22 +2854,8 @@ function CleveRoids.CountEnemiesMatching(checkFunc)
end end
end end
-- 3. Nameplate scan: visible nameplates give live GUIDs without target switching. for i = 1, 40 do
-- ClassicAPI's C_NamePlate.GetNamePlateGUIDs() lists every unit with an tryUnit("nameplate"..i)
-- 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
end end
-- During tooltip evaluation, skip known-enemy cache iteration. -- During tooltip evaluation, skip known-enemy cache iteration.
@@ -3297,9 +3171,9 @@ end
-- returns: True or false -- returns: True or false
function CleveRoids.ValidatePower(unit, operator, amount) function CleveRoids.ValidatePower(unit, operator, amount)
if not unit or not operator or not amount then return false end if not unit or not operator or not amount then return false end
local API = CleveRoids.NampowerAPI local API = CleveRoids.ClassicAPI
local power = API and API.GetUnitPower and API.GetUnitPower(unit) or UnitMana(unit) local power = API.UnitPower(unit)
local maxPower = API and API.GetUnitMaxPower and API.GetUnitMaxPower(unit) or UnitManaMax(unit) local maxPower = API.UnitPowerMax(unit)
local powerPercent = maxPower > 0 and (100 * power / maxPower) or 0 local powerPercent = maxPower > 0 and (100 * power / maxPower) or 0
if CleveRoids.operators[operator] then if CleveRoids.operators[operator] then
@@ -3309,6 +3183,26 @@ function CleveRoids.ValidatePower(unit, operator, amount)
return false return false
end 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 -- Checks whether or not the given unit has current power vs the given amount
-- unit: The unit we're checking -- unit: The unit we're checking
-- operator: valid comparitive operator symbol -- operator: valid comparitive operator symbol
@@ -3316,8 +3210,7 @@ end
-- returns: True or false -- returns: True or false
function CleveRoids.ValidateRawPower(unit, operator, amount) function CleveRoids.ValidateRawPower(unit, operator, amount)
if not unit or not operator or not amount then return false end if not unit or not operator or not amount then return false end
local API = CleveRoids.NampowerAPI local power = CleveRoids.ClassicAPI.UnitPower(unit)
local power = API and API.GetUnitPower and API.GetUnitPower(unit) or UnitMana(unit)
if power and CleveRoids.operators[operator] then if power and CleveRoids.operators[operator] then
return CleveRoids.comparators[operator](power, amount) return CleveRoids.comparators[operator](power, amount)
@@ -3326,23 +3219,15 @@ function CleveRoids.ValidateRawPower(unit, operator, amount)
return false return false
end 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) function CleveRoids.ValidateDruidRawMana(unit, operator, amount)
unit = unit or "player" unit = unit or "player"
if not operator or amount == nil then return false end if not operator or amount == nil then return false end
if (CleveRoids.playerClass ~= "DRUID") then return false end if (CleveRoids.playerClass ~= "DRUID") then return false end
-- SuperWoW returns: current-form power, caster-form mana local casterMana = CleveRoids.ClassicAPI.UnitPower(unit, 0)
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 cmp = CleveRoids.comparators and CleveRoids.comparators[operator] local cmp = CleveRoids.comparators and CleveRoids.comparators[operator]
return cmp and cmp(casterMana, amount) or false return cmp and cmp(casterMana, amount) or false
@@ -3355,10 +3240,7 @@ end
-- returns: True or false -- returns: True or false
function CleveRoids.ValidatePowerLost(unit, operator, amount) function CleveRoids.ValidatePowerLost(unit, operator, amount)
if not unit or not operator or not amount then return false end if not unit or not operator or not amount then return false end
local API = CleveRoids.NampowerAPI local powerLost = CleveRoids.ClassicAPI.UnitPowerMissing(unit)
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
if CleveRoids.operators[operator] then if CleveRoids.operators[operator] then
return CleveRoids.comparators[operator](powerLost, amount) return CleveRoids.comparators[operator](powerLost, amount)
@@ -3410,10 +3292,7 @@ end
-- returns: True or false -- returns: True or false
function CleveRoids.ValidateHpLost(unit, operator, amount) function CleveRoids.ValidateHpLost(unit, operator, amount)
if not unit or not operator or not amount then return false end if not unit or not operator or not amount then return false end
local API = CleveRoids.NampowerAPI local hpLost = CleveRoids.ClassicAPI.UnitHealthMissing(unit)
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
if CleveRoids.operators[operator] then if CleveRoids.operators[operator] then
return CleveRoids.comparators[operator](hpLost, amount) return CleveRoids.comparators[operator](hpLost, amount)
@@ -3530,7 +3409,7 @@ local function GetLowercaseSpellName(spellID)
local cached = _spellNameCache[spellID] local cached = _spellNameCache[spellID]
if cached then return cached end if cached then return cached end
local name = GetSpellRecField(spellID, "name") local name = C_Spell.GetSpellName(spellID)
if not name then return nil end if not name then return nil end
-- Strip rank and lowercase -- Strip rank and lowercase
@@ -3553,7 +3432,7 @@ local function GetSpellNames(spellID)
return cached.base, cached.full return cached.base, cached.full
end end
local fullName = GetSpellRecField(spellID, "name") local fullName = C_Spell.GetSpellName(spellID)
if not fullName then return nil, nil end if not fullName then return nil, nil end
local baseName = _string_gsub(fullName, _RANK_PATTERN, "") local baseName = _string_gsub(fullName, _RANK_PATTERN, "")
@@ -4208,7 +4087,7 @@ function CleveRoids.ValidateUnitDebuff(unit, args)
local fallbackNameLower = _string_lower(args.name) local fallbackNameLower = _string_lower(args.name)
for sid, rec in pairs(lib.objects[guid]) do for sid, rec in pairs(lib.objects[guid]) do
if rec and rec.caster == "player" then if rec and rec.caster == "player" then
local n = GetSpellRecField and GetSpellRecField(sid, "name") local n = C_Spell.GetSpellName(sid)
if n then if n then
n = CleveRoids.StripRank(n) n = CleveRoids.StripRank(n)
if _string_lower(n) == fallbackNameLower then if _string_lower(n) == fallbackNameLower then
@@ -4295,7 +4174,7 @@ function CleveRoids.ValidateUnitDebuff(unit, args)
local cleanupNameLower = _string_lower(args.name) local cleanupNameLower = _string_lower(args.name)
for sid, rec in pairs(lib.objects[guid]) do for sid, rec in pairs(lib.objects[guid]) do
if rec and rec.caster == "player" then if rec and rec.caster == "player" then
local n = GetSpellRecField and GetSpellRecField(sid, "name") local n = C_Spell.GetSpellName(sid)
if n then if n then
n = CleveRoids.StripRank(n) n = CleveRoids.StripRank(n)
if _string_lower(n) == cleanupNameLower then if _string_lower(n) == cleanupNameLower then
@@ -4629,19 +4508,9 @@ function CleveRoids.ValidatePlayerDebuff(args)
end end
function CleveRoids.ValidateWeaponImbue(slot, args) function CleveRoids.ValidateWeaponImbue(slot, args)
-- Check if weapon has enchant via API -- Temp-enchant state for this slot, read via ClassicAPI (centralizes the
local hasMainEnchant, mainExpiration, mainCharges, hasOffEnchant, offExpiration, offCharges = GetWeaponEnchantInfo() -- 12-tuple indexing; enchantID unused here but powers [mhenchant]).
local hasEnchant, expiration, charges = CleveRoids.ClassicAPI.GetWeaponEnchant(slot)
local hasEnchant, expiration, charges
if slot == "mh" then
hasEnchant = hasMainEnchant
expiration = mainExpiration
charges = mainCharges
else
hasEnchant = hasOffEnchant
expiration = offExpiration
charges = offCharges
end
-- Only consider temporary enchants (with time or charges) -- Only consider temporary enchants (with time or charges)
-- This filters out permanent enchants like Crusader, Lifestealing, etc. -- This filters out permanent enchants like Crusader, Lifestealing, etc.
@@ -4756,6 +4625,19 @@ function CleveRoids.CheckWeaponImbueByName(slot, imbueName)
return true -- No name to check return true -- No name to check
end 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 -- Create tooltip scanner if needed
if not CleveRoidsTooltip then if not CleveRoidsTooltip then
CreateFrame("GameTooltip", "CleveRoidsTooltip", nil, "GameTooltipTemplate") CreateFrame("GameTooltip", "CleveRoidsTooltip", nil, "GameTooltipTemplate")
@@ -4796,6 +4678,48 @@ function CleveRoids.CheckWeaponImbueByName(slot, imbueName)
return false return false
end 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 -- TODO: Look into https://github.com/Stanzilla/WoWUIBugs/issues/47 if needed
-- PERFORMANCE: Uncached version - called by GetCachedCooldown -- PERFORMANCE: Uncached version - called by GetCachedCooldown
function CleveRoids._GetCooldownUncached(name, ignoreGCD) function CleveRoids._GetCooldownUncached(name, ignoreGCD)
@@ -5089,7 +5013,7 @@ function CleveRoids.CheckSpellCast(unit, spell)
if not CleveRoids.spell_tracking[guid] then if not CleveRoids.spell_tracking[guid] then
return false return false
else 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 return true
end end
return false return false
@@ -5177,6 +5101,33 @@ function CleveRoids.GetSpellMechanic(spellID)
return CleveRoids.ClassicAPI.GetSpellMechanicByID(spellID) or 0 return CleveRoids.ClassicAPI.GetSpellMechanicByID(spellID) or 0
end 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.) -- Validate CC on a unit (target, focus, player, etc.)
-- Returns true if the unit has the specified CC mechanic active -- Returns true if the unit has the specified CC mechanic active
function CleveRoids.ValidateUnitCC(unit, ccType) function CleveRoids.ValidateUnitCC(unit, ccType)
@@ -5425,6 +5376,33 @@ local function ResolveFocusUnit(unit)
return unit return unit
end 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 -- A list of Conditionals and their functions to validate them
CleveRoids.Keywords = { CleveRoids.Keywords = {
exists = function(conditionals) exists = function(conditionals)
@@ -5608,6 +5586,23 @@ CleveRoids.Keywords = {
return not CleveRoids.ClassicAPI.IsStealthed() return not CleveRoids.ClassicAPI.IsStealthed()
end, 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) casting = function(conditionals)
if type(conditionals.casting) ~= "table" then return CleveRoids.CheckSpellCast(conditionals.target, "") end if type(conditionals.casting) ~= "table" then return CleveRoids.CheckSpellCast(conditionals.target, "") end
return Or(conditionals.casting, function (spell) return Or(conditionals.casting, function (spell)
@@ -5786,6 +5781,26 @@ CleveRoids.Keywords = {
end, conditionals, "noset") end, conditionals, "noset")
end, 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] — true if item exists in bags or equipped
-- [inbag:Item<12] — true if bag count of Item is less than 12 -- [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 -- Supports multi-value: [inbag:Item1/Item2 inbag:Item3] = (Item1 OR Item2) AND Item3
@@ -6008,14 +6023,14 @@ CleveRoids.Keywords = {
local groupVal = conditionals.group local groupVal = conditionals.group
-- Boolean form [group] - check if in any group -- Boolean form [group] - check if in any group
if groupVal == true then if groupVal == true then
return GetNumPartyMembers() > 0 or GetNumRaidMembers() > 0 return IsInGroup()
end end
-- Value form [group:party] or [group:raid] or [group:party/raid] -- Value form [group:party] or [group:raid] or [group:party/raid]
return Multi(groupVal, function(groupType) return Multi(groupVal, function(groupType)
if groupType == "party" then if groupType == "party" then
return GetNumPartyMembers() > 0 return IsInGroup()
elseif groupType == "raid" then elseif groupType == "raid" then
return GetNumRaidMembers() > 0 return IsInRaid()
end end
return false return false
end, conditionals, "group") end, conditionals, "group")
@@ -6027,14 +6042,14 @@ CleveRoids.Keywords = {
local groupVal = conditionals.nogroup local groupVal = conditionals.nogroup
-- Boolean form [nogroup] - check if not in any group -- Boolean form [nogroup] - check if not in any group
if groupVal == true then if groupVal == true then
return GetNumPartyMembers() == 0 and GetNumRaidMembers() == 0 return not IsInGroup()
end end
-- Value form with De Morgan's law via NegatedMulti -- Value form with De Morgan's law via NegatedMulti
return NegatedMulti(groupVal, function(groupType) return NegatedMulti(groupVal, function(groupType)
if groupType == "party" then if groupType == "party" then
return GetNumPartyMembers() == 0 return not IsInGroup()
elseif groupType == "raid" then elseif groupType == "raid" then
return GetNumRaidMembers() == 0 return not IsInRaid()
end end
return true return true
end, conditionals, "nogroup") end, conditionals, "nogroup")
@@ -6254,6 +6269,17 @@ CleveRoids.Keywords = {
end, conditionals, "mypower") end, conditionals, "mypower")
end, 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) rawpower = function(conditionals)
return Multi(conditionals.rawpower, function(args) return Multi(conditionals.rawpower, function(args)
if type(args) ~= "table" then return false end if type(args) ~= "table" then return false end
@@ -7216,6 +7242,26 @@ CleveRoids.Keywords = {
return not CleveRoids.ClassicAPI.IsSwimming() return not CleveRoids.ClassicAPI.IsSwimming()
end, 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] - Player is currently rooted (Nampower v2.36+)
rooted = function(conditionals) rooted = function(conditionals)
if not CleveRoids.NampowerAPI.features.hasPlayerIsRooted then if not CleveRoids.NampowerAPI.features.hasPlayerIsRooted then
@@ -7588,6 +7634,24 @@ CleveRoids.Keywords = {
return not CleveRoids.ValidateWeaponImbue("oh", args) return not CleveRoids.ValidateWeaponImbue("oh", args)
end, 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) immune = function(conditionals)
-- Check if target is immune to the spell being cast or damage school -- Check if target is immune to the spell being cast or damage school
-- Usage: [immune] SpellName OR [immune:SpellName] OR [immune:fire] -- Usage: [immune] SpellName OR [immune:SpellName] OR [immune:fire]
@@ -8247,7 +8311,7 @@ CleveRoids.Keywords = {
if not UnitExists(unit) then return false end if not UnitExists(unit) then return false end
return Or(conditionals.powertype, function(powerTypeName) return Or(conditionals.powertype, function(powerTypeName)
local powerType = UnitPowerType(unit) local powerType = CleveRoids.ClassicAPI.UnitPowerType(unit)
local powerTypeLower = string.lower(powerTypeName or "") local powerTypeLower = string.lower(powerTypeName or "")
if powerTypeLower == "mana" then if powerTypeLower == "mana" then
@@ -8270,7 +8334,7 @@ CleveRoids.Keywords = {
if not UnitExists(unit) then return true end if not UnitExists(unit) then return true end
return NegatedMulti(conditionals.nopowertype, function(powerTypeName) return NegatedMulti(conditionals.nopowertype, function(powerTypeName)
local powerType = UnitPowerType(unit) local powerType = CleveRoids.ClassicAPI.UnitPowerType(unit)
local powerTypeLower = string.lower(powerTypeName or "") local powerTypeLower = string.lower(powerTypeName or "")
if powerTypeLower == "mana" then if powerTypeLower == "mana" then
@@ -8355,6 +8419,33 @@ CleveRoids.Keywords = {
end, conditionals, "nomycc") end, conditionals, "nomycc")
end, 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 -- RESIST TRACKING CONDITIONALS
-- ======================================================================== -- ========================================================================
@@ -9182,6 +9273,7 @@ CleveRoids.STATIC_CONDITIONALS = {
combat = true, nocombat = true, ic = true, ooc = true, combat = true, nocombat = true, ic = true, ooc = true,
zone = true, nozone = true, zone = true, nozone = true,
stealth = true, nostealth = true, stl = true, nostl = 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, form = true, noform = true, stance = true, nostance = true,
equipped = true, noequipped = true, eq = true, noeq = true, equipped = true, noequipped = true, eq = true, noeq = true,
set = true, noset = true, set = true, noset = true,
@@ -9189,6 +9281,7 @@ CleveRoids.STATIC_CONDITIONALS = {
mod = true, nomod = true, mod = true, nomod = true,
keydown = true, nokeydown = true, keydown = true, nokeydown = true,
swimming = true, noswimming = true, swim = true, noswim = true, swimming = true, noswimming = true, swim = true, noswim = true,
indoors = true, noindoors = true, outdoors = true, nooutdoors = true,
rooted = true, norooted = true, rooted = true, norooted = true,
resting = true, noresting = true, resting = true, noresting = true,
} }
+10
View File
@@ -63,6 +63,16 @@ SlashCmdList.EQSLOT13 = CleveRoids.DoEquipTrinket1
SLASH_EQSLOT141 = "/equip14" SLASH_EQSLOT141 = "/equip14"
SlashCmdList.EQSLOT14 = CleveRoids.DoEquipTrinket2 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" SLASH_UNSHIFT1 = "/unshift"
SlashCmdList.UNSHIFT = CleveRoids.DoUnshift SlashCmdList.UNSHIFT = CleveRoids.DoUnshift
+149 -140
View File
@@ -42,6 +42,9 @@ CleveRoids.spellIdCache = {}
-- PERFORMANCE: Spell name construction cache -- PERFORMANCE: Spell name construction cache
CleveRoids.spellNameCache = {} CleveRoids.spellNameCache = {}
CleveRoids.playerGuid = UnitGUID('player')
CleveRoids.playerClass = UnitClassBase("player")
-- PERFORMANCE: Upvalues for frequently called global functions (avoid global lookups) -- PERFORMANCE: Upvalues for frequently called global functions (avoid global lookups)
local GetTime = GetTime local GetTime = GetTime
local UnitExists = UnitExists local UnitExists = UnitExists
@@ -121,6 +124,10 @@ local BOOLEAN_CONDITIONALS = {
nocombat = true, nocombat = true,
stealth = true, stealth = true,
nostealth = true, nostealth = true,
mounted = true,
nomounted = true,
standing = true,
sitting = true,
channeled = true, channeled = true,
nochanneled = true, nochanneled = true,
checkchanneled = true, checkchanneled = true,
@@ -140,6 +147,16 @@ local BOOLEAN_CONDITIONALS = {
nomhimbue = true, nomhimbue = true,
ohimbue = true, ohimbue = true,
noohimbue = 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, group = true,
nogroup = true, nogroup = true,
moving = true, moving = true,
@@ -940,19 +957,23 @@ function CleveRoids.TestForActiveAction(actions)
-- Prefer IsSpellUsable result if available (Nampower) -- Prefer IsSpellUsable result if available (Nampower)
actions.active.oom = (notEnoughPower == 1) actions.active.oom = (notEnoughPower == 1)
else else
-- SuperWoW: UnitMana returns (current power, caster mana) for druids -- Read caster mana directly for druids (the mana slot survives
local currentPower, casterMana = UnitMana("player") -- shapeshift, 0 = Enum.PowerType.Mana); other classes use their
-- primary power. Replaces the SuperWoW UnitMana 2nd-return hack.
-- For druids with SuperWoW, use caster mana for spell cost checks local manaToCheck
local manaToCheck = currentPower if CleveRoids.playerClass == "DRUID" then
if CleveRoids.playerClass == "DRUID" and type(casterMana) == "number" then manaToCheck = CleveRoids.ClassicAPI.UnitPower("player", 0)
manaToCheck = casterMana else
manaToCheck = CleveRoids.ClassicAPI.UnitPower("player")
end end
actions.active.oom = (manaToCheck < actions.active.spell.cost) actions.active.oom = (manaToCheck < actions.active.spell.cost)
end end
local start, duration = GetSpellCooldown(actions.active.spell.spellSlot, actions.active.spell.bookType) local start, duration = 0, 0
if actions.active.spell.spellSlot then
start, duration = GetSpellCooldown(actions.active.spell.spellSlot, actions.active.spell.bookType)
end
local onCooldown = (start > 0 and duration > 0) local onCooldown = (start > 0 and duration > 0)
if actions.active.isReactive then if actions.active.isReactive then
@@ -1569,18 +1590,9 @@ function CleveRoids.TryTargetFocus()
return true return true
end 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 -- 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). -- silently fall through to the next macro alternative (no warning spam).
function CleveRoids.GetFocusUnitId() 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 if UnitExists("focus") then
return "focus" return "focus"
end end
@@ -2827,26 +2839,19 @@ function CleveRoids.DoTarget(msg)
addCandidate("pettarget") addCandidate("pettarget")
if pfUI and pfUI.uf and pfUI.uf.focus and pfUI.uf.focus.label and pfUI.uf.focus.id then addCandidate("focus")
local focusTok = pfUI.uf.focus.label .. pfUI.uf.focus.id addCandidate("focustarget")
addCandidate(focusTok)
addCandidate(focusTok .. "target")
end
if CleveRoids.hasSuperwow then -- Visible nameplates as targeting candidates, via ClassicAPI's nameplateN
local numChildren = WorldFrame:GetNumChildren() -- unit tokens -- no SuperWoW WorldFrame child-walk / GUID scrape, and covers
local children = { WorldFrame:GetChildren() } -- default vanilla nameplates too. Slots are sparse (a removed plate leaves
-- its slot vacant until reused), so scan the full range rather than breaking
for i = 1, numChildren do -- on the first gap; UnitExists returns false cleanly for a free/out-of-range
local frame = children[i] -- slot.
if frame and frame:IsVisible() then for i = 1, 40 do
local success, guid = pcall(frame.GetName, frame, 1) local plate = "nameplate" .. i
if success and guid and type(guid) == "string" and string.len(guid) > 0 then if UnitExists(plate) then
if UnitExists(guid) then addCandidate(plate)
addCandidate(guid)
end
end
end
end end
end end
@@ -3588,6 +3593,32 @@ function CleveRoids.DoEquipRing2(msg)
return false return false
end 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) function CleveRoids.DoCancelForm(msg)
local handled local handled
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation -- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
@@ -3935,30 +3966,15 @@ function CleveRoids.OnUpdate(self)
CR.lastUpdate = time CR.lastUpdate = time
-- Reclaim the top of the SendChatMessage hook chain if another addon has -- LeafVillageAchievements/Legends also hook SendChatMessage and can bump ours
-- displaced our #showtooltip filter (see EnsureSendChatMessageHook). Cheap -- off the top of the chain (orphaning it / decorating #showtooltip). Reclaim
-- identity check; only re-hooks when actually displaced. -- the top when they're present so #showtooltip stays filtered.
if CR.EnsureSendChatMessageHook then if not CR._watchChatHooks then
CR.EnsureSendChatMessageHook() CR._watchChatHooks = (C_AddOns.IsAddOnLoaded("LeafVillageAchievements")
or C_AddOns.IsAddOnLoaded("LeafVillageLegends"))
end end
if CR._watchChatHooks then
-- Process deferred equipment index updates (for throttled UNIT_INVENTORY_CHANGED) CR.EnsureSendChatMessageHook()
-- 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
end end
-- PERFORMANCE: Check for expired reactive procs only if we have any -- PERFORMANCE: Check for expired reactive procs only if we have any
@@ -4176,11 +4192,7 @@ function GameTooltip.SetAction(self, slot)
local current_spell_data = CleveRoids.GetSpell(action_name) local current_spell_data = CleveRoids.GetSpell(action_name)
if current_spell_data and current_spell_data.id then if current_spell_data and current_spell_data.id then
if current_spell_data.spellSlot and current_spell_data.bookType then GameTooltip:SetSpellByID(current_spell_data.id)
GameTooltip:SetSpell(current_spell_data.spellSlot, current_spell_data.bookType)
else
GameTooltip:SetSpellByID(current_spell_data.id)
end
GameTooltip:Show() GameTooltip:Show()
return return
end end
@@ -4613,7 +4625,7 @@ function GetActionCooldown(slot)
return GetInventoryItemCooldown("player", slotId) return GetInventoryItemCooldown("player", slotId)
end end
if a.spell then if a.spell and a.spell.spellSlot then
return GetSpellCooldown(a.spell.spellSlot, a.spell.bookType) return GetSpellCooldown(a.spell.spellSlot, a.spell.bookType)
elseif a.item then elseif a.item then
if a.item.bagID and a.item.slot then if a.item.bagID and a.item.slot then
@@ -4811,8 +4823,8 @@ CleveRoids.Frame:RegisterEvent("PLAYER_ENTERING_WORLD")
CleveRoids.Frame:RegisterEvent("UPDATE_MACROS") CleveRoids.Frame:RegisterEvent("UPDATE_MACROS")
CleveRoids.Frame:RegisterEvent("SPELLS_CHANGED") CleveRoids.Frame:RegisterEvent("SPELLS_CHANGED")
CleveRoids.Frame:RegisterEvent("ACTIONBAR_SLOT_CHANGED") CleveRoids.Frame:RegisterEvent("ACTIONBAR_SLOT_CHANGED")
CleveRoids.Frame:RegisterEvent("BAG_UPDATE") CleveRoids.Frame:RegisterEvent("BAG_UPDATE_DELAYED")
CleveRoids.Frame:RegisterEvent("UNIT_INVENTORY_CHANGED") CleveRoids.Frame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
CleveRoids.Frame:RegisterEvent("UNIT_PET") CleveRoids.Frame:RegisterEvent("UNIT_PET")
-- == STATE CHANGE EVENT REGISTRATION (for performance) == -- == STATE CHANGE EVENT REGISTRATION (for performance) ==
@@ -4825,6 +4837,13 @@ CleveRoids.Frame:RegisterEvent("PLAYER_REGEN_ENABLED") -- Left actual combat (n
CleveRoids.Frame:RegisterEvent("UPDATE_SHAPESHIFT_FORM") CleveRoids.Frame:RegisterEvent("UPDATE_SHAPESHIFT_FORM")
CleveRoids.Frame:RegisterEvent("SPELL_UPDATE_COOLDOWN") CleveRoids.Frame:RegisterEvent("SPELL_UPDATE_COOLDOWN")
CleveRoids.Frame:RegisterEvent("PLAYER_STARTED_MOVING") 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 -- Use GUID events when available (v2.39+), fall back to standard per-token events
if CleveRoids.NampowerAPI.features.hasUnitGuidEvents then if CleveRoids.NampowerAPI.features.hasUnitGuidEvents then
CleveRoids.Frame:RegisterEvent("UNIT_AURA_GUID") CleveRoids.Frame:RegisterEvent("UNIT_AURA_GUID")
@@ -4939,7 +4958,6 @@ function CleveRoids.Frame:PLAYER_LOGIN()
if CleveRoids.disabled then return end if CleveRoids.disabled then return end
_, CleveRoids.playerClass = UnitClass("player") _, CleveRoids.playerClass = UnitClass("player")
_, CleveRoids.playerGuid = UnitExists("player")
CleveRoids.IndexSpells() CleveRoids.IndexSpells()
CleveRoids.IndexPetSpells() CleveRoids.IndexPetSpells()
CleveRoids.initializationTimer = GetTime() + 1.5 CleveRoids.initializationTimer = GetTime() + 1.5
@@ -5028,7 +5046,7 @@ function CleveRoids.Frame:UNIT_CASTEVENT(caster,target,action,spell_id,cast_time
-- Refresh the Judgement by updating the start time -- Refresh the Judgement by updating the start time
rec.start = GetTime() rec.start = GetTime()
local spellName = GetSpellRecField(spellID, "name") local spellName = C_Spell.GetSpellName(spellID)
local baseName = CleveRoids.StripRank(spellName) or "Unknown" local baseName = CleveRoids.StripRank(spellName) or "Unknown"
if CleveRoids.debug then if CleveRoids.debug then
@@ -5064,7 +5082,7 @@ function CleveRoids.Frame:UNIT_CASTEVENT(caster,target,action,spell_id,cast_time
-- Debug channel tracking -- Debug channel tracking
if CleveRoids.ChannelTimeDebug then 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 if string.find(spellName, "Arcane") then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[UNIT_CASTEVENT]|r %s: %s (ID:%s) caster=%s player=%s", 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))) action, spellName, tostring(spell_id), tostring(caster), tostring(CleveRoids.playerGuid)))
@@ -5137,8 +5155,8 @@ function CleveRoids.Frame:UNIT_CASTEVENT(caster,target,action,spell_id,cast_time
if CleveRoids.currentSequence and caster == CleveRoids.playerGuid then if CleveRoids.currentSequence and caster == CleveRoids.playerGuid then
local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence) local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence)
local name = GetSpellRecField(spell_id, "name") local name = C_Spell.GetSpellName(spell_id)
local rank = GetSpellRecField(spell_id, "rank") local rank = C_Spell.GetSpellSubtext(spell_id)
local nameRank = (rank and rank ~= "") and (name .. "(" .. rank .. ")") or nil local nameRank = (rank and rank ~= "") and (name .. "(" .. rank .. ")") or nil
local isSeqSpell = active and active.action and ( local isSeqSpell = active and active.action and (
active.action == name or active.action == name or
@@ -5178,7 +5196,7 @@ function CleveRoids.Frame:SPELL_CAST_EVENT(success, spellId, castType, targetGui
CleveRoids.CurrentSpell.type = "channeled" CleveRoids.CurrentSpell.type = "channeled"
CleveRoids.CurrentSpell.castingSpellId = spellId CleveRoids.CurrentSpell.castingSpellId = spellId
local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") local spellName = C_Spell.GetSpellName(spellId)
if spellName then if spellName then
CleveRoids.CurrentSpell.spellName = spellName CleveRoids.CurrentSpell.spellName = spellName
end end
@@ -5204,8 +5222,8 @@ function CleveRoids.Frame:SPELL_CAST_EVENT(success, spellId, castType, targetGui
if CleveRoids.currentSequence and success == 1 then if CleveRoids.currentSequence and success == 1 then
local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence) local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence)
if active and active.action then if active and active.action then
local name = GetSpellRecField(spellId, "name") local name = C_Spell.GetSpellName(spellId)
local rank = GetSpellRecField(spellId, "rank") local rank = C_Spell.GetSpellSubtext(spellId)
local nameRank = (rank and rank ~= "") and (name .. "(" .. rank .. ")") or nil local nameRank = (rank and rank ~= "") and (name .. "(" .. rank .. ")") or nil
local isSeqSpell = (active.action == name or (nameRank and active.action == nameRank)) local isSeqSpell = (active.action == name or (nameRank and active.action == nameRank))
@@ -5222,7 +5240,7 @@ function CleveRoids.Frame:SPELL_CAST_EVENT(success, spellId, castType, targetGui
if CleveRoids.currentSequence and success == 0 then if CleveRoids.currentSequence and success == 0 then
local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence) local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence)
if active and active.action then if active and active.action then
local name = GetSpellRecField(spellId, "name") local name = C_Spell.GetSpellName(spellId)
local isSeqSpell = (active.action == name) local isSeqSpell = (active.action == name)
if isSeqSpell then if isSeqSpell then
CleveRoids.currentSequence.status = 1 -- Reset to retry CleveRoids.currentSequence.status = 1 -- Reset to retry
@@ -5280,8 +5298,8 @@ function CleveRoids.Frame:SPELL_START_SELF(casterGuid, targetGuid, spellId, cast
if CleveRoids.currentSequence and castTimeMs and castTimeMs > 0 then if CleveRoids.currentSequence and castTimeMs and castTimeMs > 0 then
local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence) local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence)
if active and active.action then if active and active.action then
local name = GetSpellRecField(spellId, "name") local name = C_Spell.GetSpellName(spellId)
local rank = GetSpellRecField(spellId, "rank") local rank = C_Spell.GetSpellSubtext(spellId)
local nameRank = (rank and rank ~= "") and (name .. "(" .. rank .. ")") or nil local nameRank = (rank and rank ~= "") and (name .. "(" .. rank .. ")") or nil
local isSeqSpell = (active.action == name or (nameRank and active.action == nameRank)) local isSeqSpell = (active.action == name or (nameRank and active.action == nameRank))
@@ -5318,7 +5336,7 @@ function CleveRoids.Frame:SPELL_FAILED_SELF(casterGuid, targetGuid, spellId, ...
if CleveRoids.currentSequence then if CleveRoids.currentSequence then
local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence) local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence)
if active and active.action then if active and active.action then
local name = GetSpellRecField(spellId, "name") local name = C_Spell.GetSpellName(spellId)
local isSeqSpell = name and (active.action == name) local isSeqSpell = name and (active.action == name)
if isSeqSpell then if isSeqSpell then
CleveRoids.currentSequence.status = 1 -- Reset to retry CleveRoids.currentSequence.status = 1 -- Reset to retry
@@ -5357,7 +5375,7 @@ function CleveRoids.Frame:SPELLCAST_CHANNEL_START()
-- Update spell info -- Update spell info
if spellId then if spellId then
CleveRoids.CurrentSpell.castingSpellId = spellId CleveRoids.CurrentSpell.castingSpellId = spellId
local spellName = GetSpellRecField(spellId, "name") local spellName = C_Spell.GetSpellName(spellId)
if spellName then if spellName then
CleveRoids.CurrentSpell.spellName = spellName CleveRoids.CurrentSpell.spellName = spellName
end end
@@ -5437,7 +5455,7 @@ function CleveRoids.Frame:SPELLCAST_START()
-- Update spell info -- Update spell info
if spellId then if spellId then
CleveRoids.CurrentSpell.castingSpellId = spellId CleveRoids.CurrentSpell.castingSpellId = spellId
local spellName = GetSpellRecField(spellId, "name") local spellName = C_Spell.GetSpellName(spellId)
if spellName then if spellName then
CleveRoids.CurrentSpell.spellName = spellName CleveRoids.CurrentSpell.spellName = spellName
end end
@@ -5672,7 +5690,7 @@ function CleveRoids.Frame:ACTIONBAR_SLOT_CHANGED()
end end
end end
function CleveRoids.Frame:BAG_UPDATE() function CleveRoids.Frame:BAG_UPDATE_DELAYED()
-- In combat: Skip expensive indexing but still queue icon update -- In combat: Skip expensive indexing but still queue icon update
-- so conditionals like [inbag] re-evaluate (they use live bag APIs) -- so conditionals like [inbag] re-evaluate (they use live bag APIs)
if UnitAffectingCombat("player") then if UnitAffectingCombat("player") then
@@ -5701,34 +5719,22 @@ function CleveRoids.Frame:BAG_UPDATE()
end end
end end
function CleveRoids.Frame:UNIT_INVENTORY_CHANGED() function CleveRoids.Frame:PLAYER_EQUIPMENT_CHANGED()
if arg1 ~= "player" then return end -- arg1 = inventory slot that changed, arg2 = hasCurrent (slot now holds an item)
local slot, hasCurrent = arg1, arg2
-- PERFORMANCE: Invalidate equipment cache for HasGearEquipped -- In combat: skip the slot reindex to avoid lag during rapid gear swapping.
if CleveRoids.InvalidateEquipmentCache then -- [equipped] reads live engine state (C_Item.IsEquippedItem) so it stays
CleveRoids.InvalidateEquipmentCache() -- correct regardless; the Items table self-heals on the next BAG_UPDATE.
end
-- In combat: Skip ALL processing - EquipBagItem already handles cache invalidation
-- This eliminates lag from IndexEquippedItems during rapid gear swapping
if UnitAffectingCombat("player") then if UnitAffectingCombat("player") then
return return
end end
-- Out of combat: Full indexing with throttle -- Out of combat: reindex only the one slot that changed (the bag side of any
local now = GetTime() -- swap is covered by BAG_UPDATE_DELAYED). No action-bar rebuild is needed -
if (now - (CleveRoids.lastEquipIndexTime or 0)) < 0.2 then -- gear swaps don't change what's on the bars; QueueActionUpdate re-evaluates
CleveRoids.equipIndexPendingTime = now -- [equipped:...] icons/conditionals.
return CleveRoids.IndexEquipSlot(slot, hasCurrent)
end
CleveRoids.lastEquipIndexTime = now
CleveRoids.equipIndexPendingTime = nil
CleveRoids.lastItemIndexTime = now
CleveRoids.IndexItems()
CleveRoids.Actions = {}
CleveRoids.Macros = {}
CleveRoids.IndexActionBars()
if CleveRoidMacros.realtime == 0 then if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate() CleveRoids.QueueActionUpdate()
@@ -5765,6 +5771,13 @@ function CleveRoids.Frame:PLAYER_FOCUS_CHANGED()
CleveRoids.QueueActionUpdate() CleveRoids.QueueActionUpdate()
end end
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() function CleveRoids.Frame:UPDATE_SHAPESHIFT_FORM()
if CleveRoidMacros.realtime == 0 then if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate() CleveRoids.QueueActionUpdate()
@@ -5887,11 +5900,9 @@ function CleveRoids.Frame:SPELL_QUEUE_EVENT()
queueType = eventCode, queueType = eventCode,
queueTime = GetTime() queueTime = GetTime()
} }
if GetSpellRecField then local name = C_Spell.GetSpellName(spellId)
local name = GetSpellRecField(spellId, "name") if name then
if name then CleveRoids.queuedSpell.spellName = name
CleveRoids.queuedSpell.spellName = name
end
end end
-- BUGFIX: Update casting state when spell is queued (for [casting] conditional) -- BUGFIX: Update casting state when spell is queued (for [casting] conditional)
if CleveRoids.UpdateCastingState then if CleveRoids.UpdateCastingState then
@@ -5928,11 +5939,9 @@ function CleveRoids.Frame:SPELL_CAST_EVENT()
targetGuid = targetGuid, targetGuid = targetGuid,
timestamp = GetTime() timestamp = GetTime()
} }
if GetSpellRecField then local name = C_Spell.GetSpellName(spellId)
local name = GetSpellRecField(spellId, "name") if name then
if name then CleveRoids.lastCastSpell.spellName = name
CleveRoids.lastCastSpell.spellName = name
end
end end
-- Track pending cast for SPELL_GO correlation (reactive ability detection) -- Track pending cast for SPELL_GO correlation (reactive ability detection)
@@ -5962,7 +5971,7 @@ function CleveRoids.Frame:SPELL_CAST_EVENT()
if cp > 0 then if cp > 0 then
CleveRoids.pendingCasts[spellId].comboPoints = cp CleveRoids.pendingCasts[spellId].comboPoints = cp
if CleveRoids.debug then 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( DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ff88[SPELL_CAST_EVENT]|r Captured %d CP for %s (ID:%d)", string.format("|cff00ff88[SPELL_CAST_EVENT]|r Captured %d CP for %s (ID:%d)",
cp, castSpellName, spellId) cp, castSpellName, spellId)
@@ -5997,27 +6006,27 @@ function CleveRoids.Frame:KEY_UP()
CleveRoids.isActionUpdateQueued = true CleveRoids.isActionUpdateQueued = true
end end
-- Filter out the #showtooltip line that Blizzard's native macro executor sends -- Base SendChatMessage captured at first hook; used to break a hook cycle.
-- to chat when a macro runs from an action button. Named (not anonymous) so we local baseSendChatMessage = SendChatMessage
-- can detect displacement and reclaim the top of the hook chain. local sendingChatMessage = false
-- Swallow #showtooltip lines Blizzard's macro executor sends to chat.
local function CleveRoids_SendChatMessage(msg, ...) local function CleveRoids_SendChatMessage(msg, ...)
if msg and string.find(msg, "^#showtooltip") then if msg and string.find(msg, "^#showtooltip") then
return return
end end
-- Call whatever we chained over (another addon's hook, or the real function) if sendingChatMessage then
CleveRoids.Hooks.SendChatMessage(msg, unpack(arg)) -- 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 end
-- (Re-)assert our filter as the OUTERMOST SendChatMessage hook. This is a no-op -- Reassert our filter as the outermost SendChatMessage hook (no-op if already on
-- once we're already on top. It matters because some addons snapshot -- top). See the OnUpdate caller for why this is needed.
-- 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.
function CleveRoids.EnsureSendChatMessageHook() function CleveRoids.EnsureSendChatMessageHook()
if SendChatMessage ~= CleveRoids_SendChatMessage then if SendChatMessage ~= CleveRoids_SendChatMessage then
CleveRoids.Hooks.SendChatMessage = SendChatMessage CleveRoids.Hooks.SendChatMessage = SendChatMessage
@@ -6226,7 +6235,7 @@ SlashCmdList["CLEVEROID"] = function(msg)
CleveRoids_LearnedDurations = CleveRoids_LearnedDurations or {} CleveRoids_LearnedDurations = CleveRoids_LearnedDurations or {}
CleveRoids_LearnedDurations[spellID] = CleveRoids_LearnedDurations[spellID] or {} CleveRoids_LearnedDurations[spellID] = CleveRoids_LearnedDurations[spellID] or {}
CleveRoids_LearnedDurations[spellID][playerGUID] = duration 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") CleveRoids.Print("Set " .. spellName .. " (ID:" .. spellID .. ") duration to " .. duration .. "s")
else else
CleveRoids.Print("Usage: /cleveroid learn <spellID> <duration> - Manually set spell duration") CleveRoids.Print("Usage: /cleveroid learn <spellID> <duration> - Manually set spell duration")
@@ -6243,7 +6252,7 @@ SlashCmdList["CLEVEROID"] = function(msg)
else else
local spellID = tonumber(val) local spellID = tonumber(val)
if spellID and CleveRoids_LearnedDurations and CleveRoids_LearnedDurations[spellID] then 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_LearnedDurations[spellID] = nil
CleveRoids.Print("Forgot " .. spellName .. " (ID:" .. spellID .. ") duration") CleveRoids.Print("Forgot " .. spellName .. " (ID:" .. spellID .. ") duration")
elseif spellID then elseif spellID then
@@ -6429,7 +6438,7 @@ SlashCmdList["CLEVEROID"] = function(msg)
CleveRoids.Print(schoolColor .. string.upper(school) .. "|r (" .. table.getn(spellIDs) .. " spells):") CleveRoids.Print(schoolColor .. string.upper(school) .. "|r (" .. table.getn(spellIDs) .. " spells):")
for _, spellID in ipairs(spellIDs) do 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 .. ")") CleveRoids.Print(" " .. spellName .. " (ID:" .. spellID .. ")")
end end
end end
@@ -6472,7 +6481,7 @@ SlashCmdList["CLEVEROID"] = function(msg)
CleveRoids.Print("No learned combo durations yet. Cast finishers and let them expire!") CleveRoids.Print("No learned combo durations yet. Cast finishers and let them expire!")
else else
for spellID, cpData in pairs(CleveRoids_ComboDurations) do 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 .. "):") CleveRoids.Print(spellName .. " (ID:" .. spellID .. "):")
for cp = 1, 5 do for cp = 1, 5 do
if cpData[cp] then if cpData[cp] then
@@ -6554,7 +6563,7 @@ SlashCmdList["CLEVEROID"] = function(msg)
return return
end 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] local modifier = CleveRoids.talentModifiers and CleveRoids.talentModifiers[spellID]
if not modifier then if not modifier then
@@ -6630,7 +6639,7 @@ SlashCmdList["CLEVEROID"] = function(msg)
for spellID, rec in pairs(lib.objects[guid]) do for spellID, rec in pairs(lib.objects[guid]) do
if rec and rec.start and rec.duration then if rec and rec.start and rec.duration then
local timeRemaining = rec.duration + rec.start - GetTime() 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 caster = rec.caster or "unknown"
local stacks = rec.stacks or 0 local stacks = rec.stacks or 0
if timeRemaining > 0 then if timeRemaining > 0 then
@@ -6657,7 +6666,7 @@ SlashCmdList["CLEVEROID"] = function(msg)
for i = 1, 16 do for i = 1, 16 do
local texture, stacks, debuffType, spellID = UnitDebuff("target", i) local texture, stacks, debuffType, spellID = UnitDebuff("target", i)
if texture then 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)", CleveRoids.Print(string.format(" Slot %d: [%d] %s (stacks: %d)",
i, spellID or 0, spellName, stacks or 0)) i, spellID or 0, spellName, stacks or 0))
debuffCount = debuffCount + 1 debuffCount = debuffCount + 1
@@ -6677,7 +6686,7 @@ SlashCmdList["CLEVEROID"] = function(msg)
-- Check if this might be an overflow debuff by checking libdebuff durations -- Check if this might be an overflow debuff by checking libdebuff durations
local isDebuff = lib and lib.durations and lib.durations[spellID] local isDebuff = lib and lib.durations and lib.durations[spellID]
if isDebuff then 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", CleveRoids.Print(string.format(" Buff Slot %d (=Debuff %d): [%d] %s (stacks: %d) |cffff8800OVERFLOW|r",
i, i + 16, spellID, spellName, stacks or 0)) i, i + 16, spellID, spellName, stacks or 0))
overflowCount = overflowCount + 1 overflowCount = overflowCount + 1
@@ -6718,7 +6727,7 @@ SlashCmdList["CLEVEROID"] = function(msg)
-- Also check GetSpellRecField -- Also check GetSpellRecField
if GetSpellRecField then if GetSpellRecField then
for id = 1, 30000 do 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 if name and string.lower(name) == string.lower(searchName) then
local found = false local found = false
for _, existingID in ipairs(foundIDs) do for _, existingID in ipairs(foundIDs) do
+4 -4
View File
@@ -209,8 +209,8 @@ local function InjectCustomSpells()
local count = 0 local count = 0
for spellID, data in pairs(CleveRoids.CustomCursiveSpells) do for spellID, data in pairs(CleveRoids.CustomCursiveSpells) do
-- Get texture from GetSpellRecField + GetSpellIconTexture -- Get texture from GetSpellRecField + GetSpellIconTexture
local name = GetSpellRecField(spellID, "name") local name = C_Spell.GetSpellName(spellID)
local rank = GetSpellRecField(spellID, "rank") local rank = C_Spell.GetSpellSubtext(spellID)
local texture = CleveRoids.libdebuff and CleveRoids.libdebuff:GetCachedIcon(spellID) local texture = CleveRoids.libdebuff and CleveRoids.libdebuff:GetCachedIcon(spellID)
if texture then if texture then
-- Always update/add (in case Cursive reloaded and cleared them) -- Always update/add (in case Cursive reloaded and cleared them)
@@ -524,8 +524,8 @@ CleveRoids.HandleConsoleCommand = function(msg)
return return
end end
local name = GetSpellRecField(spellID, "name") local name = C_Spell.GetSpellName(spellID)
local rank = GetSpellRecField(spellID, "rank") local rank = C_Spell.GetSpellSubtext(spellID)
local texture = CleveRoids.libdebuff and CleveRoids.libdebuff:GetCachedIcon(spellID) local texture = CleveRoids.libdebuff and CleveRoids.libdebuff:GetCachedIcon(spellID)
if not name then if not name then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Error:|r Spell ID " .. spellID .. " not found.") DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Error:|r Spell ID " .. spellID .. " not found.")
+3 -13
View File
@@ -1391,12 +1391,6 @@ end
-- Extension Entry Points -- Extension Entry Points
-- ============================================================================ -- ============================================================================
function Extension.OnAddonLoaded()
if arg1 == "Blizzard_MacroUI" then
InstallHooks()
end
end
function Extension.OnLoad() function Extension.OnLoad()
-- Skip if macro checker is disabled -- Skip if macro checker is disabled
if CleveRoidMacros and CleveRoidMacros.macrocheck == 0 then return end 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) -- Skip if SuperMacro is loaded (detected at load time)
if SuperMacroFrame ~= nil then return end if SuperMacroFrame ~= nil then return end
-- Listen for macro UI loading -- Install once Blizzard's macro UI is available (fires immediately if already
Extension.RegisterEvent("ADDON_LOADED", "OnAddonLoaded") -- loaded), replacing the ADDON_LOADED listener + manual "already loaded" check.
EventUtil.ContinueOnAddOnLoaded("Blizzard_MacroUI", InstallHooks)
-- If MacroFrame already exists (unlikely but safe), hook immediately
if MacroFrame and MacroFrameText then
InstallHooks()
end
end end
_G["CleveRoids"] = CleveRoids _G["CleveRoids"] = CleveRoids
+4 -15
View File
@@ -133,12 +133,6 @@ function Extension.OnMacroFrameLoad()
end end
end end
function Extension.OnAddonLoaded()
if arg1 == "Blizzard_MacroUI" then
Extension.OnMacroFrameLoad()
end
end
function Extension.OnLoad() function Extension.OnLoad()
-- Schedule messages to show after UI is ready -- Schedule messages to show after UI is ready
local function ShowMessages() local function ShowMessages()
@@ -178,16 +172,11 @@ function Extension.OnLoad()
end end
end end
-- Listen for macro UI loading -- Hook the macro UI once available (fires immediately if already loaded).
Extension.RegisterEvent("ADDON_LOADED", "OnAddonLoaded") EventUtil.ContinueOnAddOnLoaded("Blizzard_MacroUI", Extension.OnMacroFrameLoad)
-- Also try to hook MacroFrame_SaveMacro if it already exists -- Status messages on login (currently disabled inside OnPlayerLogin).
if MacroFrame_SaveMacro then EventUtil.ContinueOnPlayerLogin(Extension.OnPlayerLogin)
Extension.OnMacroFrameLoad()
end
-- Register PLAYER_LOGIN to show status messages
Extension.RegisterEvent("PLAYER_LOGIN", "OnPlayerLogin")
-- Store the message function for later -- Store the message function for later
Extension.ShowMessages = ShowMessages Extension.ShowMessages = ShowMessages
+4 -6
View File
@@ -6,7 +6,6 @@ local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {} local CleveRoids = _G.CleveRoids or {}
local Extension = CleveRoids.RegisterExtension("CT_RaidAssist") local Extension = CleveRoids.RegisterExtension("CT_RaidAssist")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnter() function Extension.OnEnter()
local tempOptions = CT_RAMenu_Options["temp"] local tempOptions = CT_RAMenu_Options["temp"]
@@ -48,15 +47,14 @@ function Extension.OnLeave()
CleveRoids.ClearMouseoverFrom("native") CleveRoids.ClearMouseoverFrom("native")
end end
function Extension.OnLoad() function Extension.OnLoad() end
if arg1 ~= "CT_RaidAssist" then
return
end
function Extension.OnAddOnLoad()
if not CT_RA_MemberFrame_OnEnter then return end
Extension.Hook("CT_RA_MemberFrame_OnEnter", "OnEnter") Extension.Hook("CT_RA_MemberFrame_OnEnter", "OnEnter")
Extension.HookMethod(_G["GameTooltip"], "Hide", "OnLeave") Extension.HookMethod(_G["GameTooltip"], "Hide", "OnLeave")
Extension.HookMethod(_G["GameTooltip"], "FadeOut", "OnLeave") Extension.HookMethod(_G["GameTooltip"], "FadeOut", "OnLeave")
end 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 CleveRoids = _G.CleveRoids or {}
local Extension = CleveRoids.RegisterExtension("CT_UnitFrames") local Extension = CleveRoids.RegisterExtension("CT_UnitFrames")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.SetHook(widget) function Extension.SetHook(widget)
local hookedOnEnter = widget:GetScript("OnEnter") local hookedOnEnter = widget:GetScript("OnEnter")
@@ -24,8 +23,10 @@ function Extension.SetHook(widget)
end) end)
end end
function Extension.OnLoad() function Extension.OnLoad() end
if arg1 ~= "CT_UnitFrames" or not CT_AssistFrame then
function Extension.OnAddOnLoad()
if not CT_AssistFrame then
return return
end end
CleveRoids.Print("CT_UnitFrames module loaded.") CleveRoids.Print("CT_UnitFrames module loaded.")
@@ -36,4 +37,4 @@ function Extension.OnLoad()
Extension.SetHook(CT_AssistFrame_Drag) Extension.SetHook(CT_AssistFrame_Drag)
end 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 {} CleveRoids.Hooks = CleveRoids.Hooks or {}
local Extension = CleveRoids.RegisterExtension("CursiveMouseover") local Extension = CleveRoids.RegisterExtension("CursiveMouseover")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
local hooked = false local hooked = false
@@ -66,21 +65,13 @@ local function HookCursiveUI()
end end
function Extension.OnLoad() function Extension.OnLoad()
-- Try to hook when Cursive loads -- Delay slightly to ensure Cursive.ui is initialized
if arg1 == "Cursive" then local frame = CreateFrame("Frame")
-- Delay slightly to ensure Cursive.ui is initialized frame:SetScript("OnUpdate", function()
local frame = CreateFrame("Frame") if HookCursiveUI() then
frame:SetScript("OnUpdate", function() this:Hide()
if HookCursiveUI() then end
this:Hide() end)
end
end)
end
end end
-- Also try to hook immediately in case Cursive is already loaded EventUtil.ContinueOnAddOnLoaded("Cursive", Extension.OnLoad)
if Cursive and Cursive.ui then
HookCursiveUI()
end
_G["CleveRoids"] = CleveRoids
+4 -6
View File
@@ -6,7 +6,6 @@ local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {} local CleveRoids = _G.CleveRoids or {}
local Extension = CleveRoids.RegisterExtension("DiscordUnitFrames") local Extension = CleveRoids.RegisterExtension("DiscordUnitFrames")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnterFrame() function Extension.OnEnterFrame()
CleveRoids.SetMouseoverFrom("duf", this.unit) CleveRoids.SetMouseoverFrom("duf", this.unit)
@@ -26,11 +25,10 @@ function Extension.OnLeaveElement()
CleveRoids.ClearMouseoverFrom("native") CleveRoids.ClearMouseoverFrom("native")
end end
function Extension.OnLoad() function Extension.OnLoad() end
if arg1 ~= "DiscordUnitFrames" then
return
end
function Extension.OnAddOnLoad()
if not DUF_UnitFrame_OnEnter then return end
CleveRoids.ClearHooks() CleveRoids.ClearHooks()
Extension.Hook("DUF_UnitFrame_OnEnter", "OnEnterFrame") Extension.Hook("DUF_UnitFrame_OnEnter", "OnEnterFrame")
Extension.Hook("DUF_UnitFrame_OnLeave", "OnLeaveFrame") Extension.Hook("DUF_UnitFrame_OnLeave", "OnLeaveFrame")
@@ -39,4 +37,4 @@ function Extension.OnLoad()
Extension.Hook("DUF_Element_OnLeave", "OnLeaveElement") Extension.Hook("DUF_Element_OnLeave", "OnLeaveElement")
end 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 {} CleveRoids.Hooks = CleveRoids.Hooks or {}
local Extension = CleveRoids.RegisterExtension("Grid") local Extension = CleveRoids.RegisterExtension("Grid")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnter(unit) function Extension.OnEnter(unit)
CleveRoids.SetMouseoverFrom("grid", unit) CleveRoids.SetMouseoverFrom("grid", unit)
@@ -19,11 +18,10 @@ function Extension.OnLeave()
CleveRoids.ClearMouseoverFrom("native") CleveRoids.ClearMouseoverFrom("native")
end end
function Extension.OnLoad() function Extension.OnLoad() end
if arg1 ~= "Grid" then
return
end
function Extension.OnAddOnLoad()
if not GridFrame then return end
CleveRoids.Hooks.Grid = { CreateFrames = GridFrame.frameClass.prototype.CreateFrames} CleveRoids.Hooks.Grid = { CreateFrames = GridFrame.frameClass.prototype.CreateFrames}
GridFrame.frameClass.prototype.CreateFrames = CleveRoids.GrdCreateFrames GridFrame.frameClass.prototype.CreateFrames = CleveRoids.GrdCreateFrames
end end
@@ -111,4 +109,4 @@ function CleveRoids:GrdCreateFrames()
ClickCastFrames[self.frame] = true ClickCastFrames[self.frame] = true
end 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 CreateFrames = nil
local Extension = CleveRoids.RegisterExtension("NotGrid") local Extension = CleveRoids.RegisterExtension("NotGrid")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnter() function Extension.OnEnter()
CleveRoids.SetMouseoverFrom("ngrid", this.unit) CleveRoids.SetMouseoverFrom("ngrid", this.unit)
@@ -39,7 +38,9 @@ function CleveRoids:NotGrid_CreateFrames()
end end
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. -- NotGrid loads before CleveRoids, so if NotGrid is enabled, then it's global will exist.
if not NotGrid then if not NotGrid then
return return
@@ -48,7 +49,6 @@ function Extension.OnLoad()
CreateFrames = NotGrid.CreateFrames CreateFrames = NotGrid.CreateFrames
NotGrid.CreateFrames = CleveRoids.NotGrid_CreateFrames NotGrid.CreateFrames = CleveRoids.NotGrid_CreateFrames
Extension.UnregisterEvent("ADDON_LOADED", "Onload")
end 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 {} CleveRoids.Hooks = CleveRoids.Hooks or {}
local Extension = CleveRoids.RegisterExtension("PerfectRaid") local Extension = CleveRoids.RegisterExtension("PerfectRaid")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnter(unit) function Extension.OnEnter(unit)
CleveRoids.SetMouseoverFrom("praid", unit) CleveRoids.SetMouseoverFrom("praid", unit)
@@ -19,11 +18,10 @@ function Extension.OnLeave()
CleveRoids.ClearMouseoverFrom("native") CleveRoids.ClearMouseoverFrom("native")
end end
function Extension.OnLoad() function Extension.OnLoad() end
if arg1 ~= "PerfectRaid" then
return
end
function Extension.OnAddOnLoad()
if not PerfectRaid then return end
CleveRoids.Hooks.PerfectRaid = { CreateFrame = PerfectRaid.CreateFrame } CleveRoids.Hooks.PerfectRaid = { CreateFrame = PerfectRaid.CreateFrame }
PerfectRaid.CreateFrame = CleveRoids.PerfectRaidCreateFrame PerfectRaid.CreateFrame = CleveRoids.PerfectRaidCreateFrame
end end
@@ -134,4 +132,4 @@ function CleveRoids.PerfectRaidCreateFrame(self, num)
--]] --]]
end 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 {} CleveRoids.Hooks = CleveRoids.Hooks or {}
local Extension = CleveRoids.RegisterExtension("ag_UnitFrames") local Extension = CleveRoids.RegisterExtension("ag_UnitFrames")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnter(unit) function Extension.OnEnter(unit)
CleveRoids.SetMouseoverFrom("aguf", 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} 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.OnEnter = CleveRoids.aUFOnEnter
aUF.classes.aUFunit.prototype.OnLeave = CleveRoids.aUFOnLeave aUF.classes.aUFunit.prototype.OnLeave = CleveRoids.aUFOnLeave
Extension.UnregisterEvent("ADDON_LOADED", "Onload")
end end
-- Taken from ag_UnitClass.lua -- Taken from ag_UnitClass.lua
@@ -41,4 +39,4 @@ function CleveRoids:aUFOnLeave()
end end
_G["CleveRoids"] = CleveRoids EventUtil.ContinueOnAddOnLoaded("ag_UnitFrames", Extension.OnLoad)
+3 -4
View File
@@ -8,6 +8,9 @@
- Resolving a real UnitID when .unit isn't set - 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" - 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 _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {} local CleveRoids = _G.CleveRoids or {}
@@ -31,7 +34,6 @@ local function ResolvePfUnit(frame, fallbackName)
name = strlower(name) name = strlower(name)
local candidates = { "target", "targettarget", "player", "pet" } local candidates = { "target", "targettarget", "player", "pet" }
local i
for i = 1, 4 do for i = 1, 4 do
table.insert(candidates, "party"..i) table.insert(candidates, "party"..i)
table.insert(candidates, "partypet"..i) table.insert(candidates, "partypet"..i)
@@ -153,7 +155,6 @@ end
function Extension.RegisterPartyScripts() function Extension.RegisterPartyScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.group then return end if not pfUI or not pfUI.uf or not pfUI.uf.group then return end
local i
for i = 0, 4 do for i = 0, 4 do
local frame = pfUI.uf.group[i] local frame = pfUI.uf.group[i]
if frame then if frame then
@@ -181,7 +182,6 @@ end
function Extension.RegisterRaidScripts() function Extension.RegisterRaidScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.raid then return end if not pfUI or not pfUI.uf or not pfUI.uf.raid then return end
local i
for i = 1, 40 do for i = 1, 40 do
local frame = pfUI.uf.raid[i] local frame = pfUI.uf.raid[i]
if frame then if frame then
@@ -333,7 +333,6 @@ end
function Extension.RegisterRaidMarkScripts() function Extension.RegisterRaidMarkScripts()
if not pfUI or not pfUI.raidmarkers or not pfUI.raidmarkers.rows then return end if not pfUI or not pfUI.raidmarkers or not pfUI.raidmarkers.rows then return end
local i
for i = 1, 8 do for i = 1, 8 do
local row = pfUI.raidmarkers.rows[i] local row = pfUI.raidmarkers.rows[i]
if row then if row then
+4 -6
View File
@@ -6,7 +6,6 @@ local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {} local CleveRoids = _G.CleveRoids or {}
local Extension = CleveRoids.RegisterExtension("sRaidFrames") local Extension = CleveRoids.RegisterExtension("sRaidFrames")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension:OnEnter(frame) function Extension:OnEnter(frame)
CleveRoids.SetMouseoverFrom("sraid", frame.unit) CleveRoids.SetMouseoverFrom("sraid", frame.unit)
@@ -17,14 +16,13 @@ function Extension.OnLeave()
CleveRoids.ClearMouseoverFrom("native") CleveRoids.ClearMouseoverFrom("native")
end end
function Extension.OnLoad() function Extension.OnLoad() end
if arg1 ~= "sRaidFrames" then
return
end
function Extension.OnAddOnLoad()
if not sRaidFrames then return end
Extension.HookMethod(sRaidFrames, "UnitTooltip", "OnEnter") Extension.HookMethod(sRaidFrames, "UnitTooltip", "OnEnter")
Extension.HookMethod(_G["GameTooltip"], "Hide", "OnLeave") Extension.HookMethod(_G["GameTooltip"], "Hide", "OnLeave")
Extension.HookMethod(_G["GameTooltip"], "FadeOut", "OnLeave") Extension.HookMethod(_G["GameTooltip"], "FadeOut", "OnLeave")
end 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") GameTooltip:SetOwner(btn, "ANCHOR_BOTTOMLEFT")
local spellName local spellName
if data.spellId then 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 else
spellName = data.displayName spellName = data.displayName
end end
@@ -649,7 +649,7 @@ local function InjectTestTargetData()
for i = 1, table.getn(TEST_TARGET_SPELL_IDS) do for i = 1, table.getn(TEST_TARGET_SPELL_IDS) do
local spellId = TEST_TARGET_SPELL_IDS[i] local spellId = TEST_TARGET_SPELL_IDS[i]
local dur = targetDurations[i] or 60 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 if not CleveRoids.AllCasterAuraTracking[targetGuid][spellName] then
CleveRoids.AllCasterAuraTracking[targetGuid][spellName] = {} CleveRoids.AllCasterAuraTracking[targetGuid][spellName] = {}
end end
+45 -44
View File
@@ -156,53 +156,27 @@ end
-- Lightweight equipment-only indexing for combat situations -- Lightweight equipment-only indexing for combat situations
-- Updates existing cache rather than rebuilding it -- 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 -- PERFORMANCE: Index a single equipment slot instead of all 20
-- Use when we know exactly which slot changed (e.g., from EquipBagItem) -- Use when we know exactly which slot changed (e.g., from EquipBagItem or
function CleveRoids.IndexEquipSlot(inventoryID) -- 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 if not inventoryID then return end
local items = CleveRoids.Items or {} 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 if itemID then
local name, itemLink, _, _, itemType, itemSubType, _, _, texture = GetItemInfo(itemID) local name, itemLink, _, _, itemType, itemSubType, _, _, texture = GetItemInfo(itemID)
if name then if name then
@@ -378,9 +352,9 @@ function CleveRoids.GetActionButtonInfo(slot)
if not actionType then return end if not actionType then return end
if actionType == "spell" and id then if actionType == "spell" and id then
local rank = GetSpellRecField(id, "rank") local rank = C_Spell.GetSpellSubtext(id)
if rank == "" then rank = nil end 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 elseif actionType == "item" and id then
local item = CleveRoids.GetItem(id) local item = CleveRoids.GetItem(id)
return "ITEM", id, (item and item.name) return "ITEM", id, (item and item.name)
@@ -447,6 +421,27 @@ end
function CleveRoids.GetSpell(text) function CleveRoids.GetSpell(text)
text = CleveRoids.Trim(text) text = CleveRoids.Trim(text)
-- Explicit "spell:<id>" form. If the player knows the spell, reuse its cached
-- spellbook entry (full cost/cooldown/usability). FindSpellBookSlotByID
-- (ClassicAPI) resolves per-rank IDs and pet spells natively and returns the
-- same bookType string CleveRoids.Spells is keyed by. If the spell is not in
-- either book, fall back to an id-only entry that SetAction renders via
-- SetSpellByID; cost=0 keeps the downstream active-action checks nil-safe.
local _, _, spellId = string.find(text, "^spell:(%d+)$")
if spellId then
spellId = tonumber(spellId)
local slot, book = FindSpellBookSlotByID(spellId)
if slot then
local name, rank = GetSpellInfo(slot, book)
local byName = name and CleveRoids.Spells[book] and CleveRoids.Spells[book][name]
if byName then
return (rank and rank ~= "" and byName[rank]) or byName.highest or byName
end
end
return { id = spellId, texture = C_Spell.GetSpellTexture(spellId) or CleveRoids.unknownTexture, cost = 0 }
end
local rs, _, rank = string.find(text, "[^%s]%((Rank %d+)%)$") local rs, _, rank = string.find(text, "[^%s]%((Rank %d+)%)$")
local name = rank and string.sub(text, 1, rs) or text local name = rank and string.sub(text, 1, rs) or text
@@ -534,6 +529,12 @@ end
function CleveRoids.GetItem(text) function CleveRoids.GetItem(text)
if not text or text == "" then return end if not text or text == "" then return end
-- Explicit "item:<id>" form: force resolution by item ID. Reuses the numeric
-- lookup below (equipped -> bags -> GetItemInfo), so location-aware tooltip
-- rendering (SetInventoryItem/SetBagItem) still applies when the player has it.
local _, _, prefixedId = string.find(text, "^item:(%d+)$")
if prefixedId then text = prefixedId end
local Items = CleveRoids.Items local Items = CleveRoids.Items
local item = Items[text] or Items[tostring(text)] local item = Items[text] or Items[tostring(text)]
if not item then if not item then
+4
View File
@@ -651,6 +651,8 @@ local function validateConditional(conditional, args, action)
combo = true, combo = true,
hp = true, myhp = true, rawhp = true, myrawhp = true, hp = true, myhp = true, rawhp = true, myrawhp = true,
power = true, mypower = true, rawpower = true, myrawpower = 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, hplost = true, myhplost = true,
powerlost = true, mypowerlost = true, powerlost = true, mypowerlost = true,
stat = true, stat = true,
@@ -678,6 +680,8 @@ local function validateConditional(conditional, args, action)
-- Check operator syntax for numeric comparisons -- Check operator syntax for numeric comparisons
if args and type(args) == "string" then if args and type(args) == "string" then
local hasHpOrPower = safeStringFind(baseCond, "hp") or safeStringFind(baseCond, "power") or 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" safeStringFind(baseCond, "combo") or baseCond == "stat"
if hasHpOrPower then if hasHpOrPower then
local hasOperator = safeStringFind(args, "[<>=~]+") local hasOperator = safeStringFind(args, "[<>=~]+")
+2 -41
View File
@@ -71,7 +71,6 @@
Spell Miss Events (v2.31+): Spell Miss Events (v2.31+):
- SPELL_MISS_SELF / SPELL_MISS_OTHER - Spell miss/resist/immune/dodge/etc. - 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+): Aura Event State Parameter (v2.32+):
- Buff/debuff events include 7th `state` parameter (0=added, 1=removed, 2=modified) - Buff/debuff events include 7th `state` parameter (0=added, 1=removed, 2=modified)
@@ -364,9 +363,8 @@ API.VERSION_REQUIREMENTS = {
["AuraDurationEvents"] = { 2, 30, 0 }, ["AuraDurationEvents"] = { 2, 30, 0 },
["GetPlayerAuraDuration"] = { 2, 30, 0, "GetPlayerAuraDuration" }, ["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 ["SpellMissEvents"] = { 2, 31, 0 }, -- SPELL_MISS_SELF/OTHER events
["GetSpellPower"] = { 2, 31, 0, "GetSpellPower" },
-- v2.32+ - Aura event state parameter and stack removal fix -- v2.32+ - Aura event state parameter and stack removal fix
["AuraEventState"] = { 2, 32, 0 }, ["AuraEventState"] = { 2, 32, 0 },
@@ -575,9 +573,8 @@ local function InitializeFeatures()
f.hasAuraDurationEvents = API.HasFeature("AuraDurationEvents") f.hasAuraDurationEvents = API.HasFeature("AuraDurationEvents")
f.hasGetPlayerAuraDuration = API.HasFeature("GetPlayerAuraDuration") f.hasGetPlayerAuraDuration = API.HasFeature("GetPlayerAuraDuration")
-- v2.31+ Spell miss events and spell power -- v2.31+ Spell miss events
f.hasSpellMissEvents = API.HasFeature("SpellMissEvents") f.hasSpellMissEvents = API.HasFeature("SpellMissEvents")
f.hasGetSpellPower = API.HasFeature("GetSpellPower")
-- v2.32+ Aura event state parameter -- v2.32+ Aura event state parameter
f.hasAuraEventState = API.HasFeature("AuraEventState") f.hasAuraEventState = API.HasFeature("AuraEventState")
@@ -3387,15 +3384,6 @@ API.MISS_INFO = {
-- SPELL POWER QUERY (v2.31+) -- 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+) -- Get duration of a spell in milliseconds (v2.38+)
-- For channeling spells: returns the channel duration. -- For channeling spells: returns the channel duration.
@@ -3504,32 +3492,5 @@ function API.GetUnitMaxHealth(unitToken)
return UnitHealthMax(unitToken) return UnitHealthMax(unitToken)
end 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 -- Expose API globally for other addons
_G.CleveRoidsNampowerAPI = API _G.CleveRoidsNampowerAPI = API
+180 -226
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). - class-aware `[dispellable]` (only types this character can actually remove).
- optional name filtering on the type keywords (e.g. `[magic:Polymorph]`). - optional name filtering on the type keywords (e.g. `[magic:Polymorph]`).
### 2. `C_Item.GetWeaponEnchantInfo()` — temp-enchant IDs ### 2. ~~`C_Item.GetWeaponEnchantInfo()` — temp-enchant IDs~~ — DONE
- **API:** `API.md:7159` — returns 12-tuple including `enchantID` for main/off/ranged. - Shipped as `[mhenchant]` / `[ohenchant]` (+ `no` variants). Matches the applied
- **Unlocks:** detect *which* temp enchant (poison/oil/sharpening stone) is temp enchant by SpellItemEnchantment ID *or* localized name — the name path
applied to a weapon, not just that one exists. resolves via `C_Item.GetEnchantInfo(id).name` (the ID→name table this doc
- **Naming:** `[poison]` is now taken by the target dispel-type conditional assumed we lacked), so no tooltip scan. Wrappers `ClassicAPI.GetWeaponEnchant`
(slice 1 above). Use weapon-specific keywords for this — e.g. / `GetEnchantName`; `ValidateWeaponImbue` now reads through the former.
`[mhenchant:<id>]` / `[ohenchant:<id>]` (or `[mhpoison:<id>]`/`[ohpoison:<id>]`), - Bare = any temp enchant; OR-lists supported (`[mhenchant:2823/Deadly_Poison]`).
not a bare `[poison]`. - Follow-up (optional): route `[mhimbue:Name]`'s match through `GetEnchantName`
- Vanilla's global only reports presence; this is a genuinely new capability too, retiring the green-text tooltip scan in `CheckWeaponImbueByName`.
(rogue/shaman/enhance).
### 3. `GetUnitSpeed(unit)` + `IsFalling()` / `IsSwimming()` ### 3. `GetUnitSpeed(unit)` + `IsFalling()` / `IsSwimming()`
- **API:** `API.md:8312`, `API.md:7629`. - **API:** `API.md:8312`, `API.md:7629`.