46 Commits

Author SHA1 Message Date
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
Brues d060b04908 Reclaim SendChatMessage hook so #showtooltip stays filtered
Our #showtooltip filter hooks the global SendChatMessage. An addon that
snapshots SendChatMessage at its own file-load and later calls that
snapshot directly (e.g. LeafVillageAchievements at PLAYER_ENTERING_WORLD+3s)
orphans our hook if it loaded before us -- its snapshot predates our filter,
so #showtooltip leaks to chat. This bites the fork specifically: it sorts
after LeafVillageAchievements alphabetically, so it loads too late to be in
the snapshot, whereas upstream "CleveRoidMacros" sorted before it.

Make the filter a named function and add EnsureSendChatMessageHook, which
re-asserts it as the outermost SendChatMessage hook (no-op once on top).
OnUpdate calls it each throttled tick via a cheap identity check, so we
reclaim the top of the chain within a frame of being displaced. Normal
messages still flow through the chained-over hook untouched.
2026-07-24 09:41:26 -05:00
Brues 6ac69501f5 pass base object rather than ItemLocation 2026-07-20 10:35:09 -05:00
Brues 782c685fba Replace tooltip-scanning GetSpellCost with ClassicAPI DBC reads
GetSpellCost now reads power cost and reagents straight from Spell.dbc via
C_Spell.GetSpellPowerCost (effective, talent-modified cost) and
C_Spell.GetSpellReagents (itemID), dropping the GameTooltip owner/scan
frames, their font strings, and all locale-dependent line parsing.

Reagent counting is now itemID-based end to end: GetReagentCount takes an
itemID and matches by id in the Items cache / bag scan. This removes the
hardcoded English reagent tables (_ReagentBySpell, _ReagentIdByName) and
the name-matching bag-scan tooltip, so it works on any client locale.
Verified in-game that GetSpellReagents covers DBC reagent spells (Vanish
-> Flash Powder), which the hand table previously special-cased.

The localized reagent name is still used for countedItemTypes registration
(recognizing a reagent item placed on the action bar); when that name
isn't cached yet, warm it via the ClassicAPI Item mixin
(Item:CreateFromItemID/ContinueOnItemLoad) and register it once it lands.
2026-07-19 16:34:19 -05:00
Brues 97e898f14c Add /cancelform command and CancelShapeshiftForm
Adds a new /cancelform slash command (Console.lua) wired to CleveRoids.DoCancelForm. Replaces the previous _unshiftAction with _cancelFormAction that calls CancelShapeshiftForm, and updates DoUnshift to delegate to DoCancelForm for backwards compatibility. Also fixes a unit name check by replacing GetUnitName with UnitName in DoRetarget.
2026-07-19 14:32:13 -05:00
Brues b84835a567 Use GetInventoryItemID instead of string parsing 2026-07-17 21:25:25 -05:00
Brues 40cf2c8010 More ClassicAPI porting 2026-07-15 19:45:31 -05:00
Brues a5a0b9c00a Adopt ClassicAPI C_Item.GetItemName in Utility.lua equip-modifier debug
Replace the debug-only GetInventoryItemLink name scrape with the
decorated C_Item.GetItemName location form.
2026-07-15 19:36:18 -05:00
Brues 6e57ff01dc Adopt ClassicAPI C_Item item reads in Tooltip/Generic.lua indexers
Migrate the link-scrape-then-discard sites to direct ClassicAPI reads:

- IndexEquippedItems / IndexEquipSlot: GetInventoryItemID for the id
  (GetItemInfo still supplies the stored base link/texture)
- IndexItems bag+equip scans: GetContainerItemID/GetInventoryItemID +
  C_Item.GetItemName (decorated) for the dedup fast-path; drop the now
  dead GetContainerItemLink/GetInventoryItemLink locals
- GetItemFast cache validation: alloc-free id compare via
  GetInventoryItemID/GetContainerItemID, decorated-name compare otherwise
- IsItemEquipped: GetInventoryItemID + C_Item.GetItemName

Left untouched: makeInventoryItem/makeBagItem and the GetItem/GetItemFast
scan loops, which build a real link that is stored on the item and later
fed to GameTooltip:SetHyperlink.

Note: the equipped-scan loops now start at slot 1 (GetInventoryItemID
requires slot>=1), so the ammo slot (0) is no longer walked by these
indexers -- ammo is not referenced by name anywhere in the addon.
2026-07-15 19:33:07 -05:00
Brues 9cd79d682d Adopt ClassicAPI C_Item item reads in Conditionals.lua
Replace GetInventoryItemLink/GetContainerItemLink link-scraping with
direct ClassicAPI reads across the item conditionals:

- [equipped] cache build: GetInventoryItemID + C_Item.GetItemName
  (decorated name replaces the old bracket-name / GetItemInfo two-step)
- slot-number -> item name resolves ([cd], [usable]/[nousable]):
  C_Item.GetItemName location form
- HasItem / GetItemCooldown substring fallbacks: match the decorated
  name instead of the raw link string
- numeric-slot presence check: GetInventoryItemID

Suffix decoration is preserved via the location form, so partial/
suffixed [equipped:...] matches behave as before.
2026-07-15 19:17:00 -05:00
Brues f54eda5ae3 Adopt ClassicAPI C_Item/C_Container item reads in Core.lua
Replace GetInventoryItemLink/GetContainerItemLink link-scraping (built
solely to regex out item:ID or the bracketed name) with direct ClassicAPI
reads:

- id/presence: GetInventoryItemID, C_Container.GetContainerItemID
- suffix-sensitive names (equip-by-name on gear): C_Item.GetItemName
  location form, which now carries random-suffix decoration
- suffix-free names (consumables/reagents/poisons) in hot bag scans:
  id + C_Item.GetItemNameByID (base name, zero table allocation)

Also covers /use equipped-slot resolution and the WDB warm scans. Drops
the now-dead GetContainerItemLink/GetInventoryItemLink/string_find
upvalue aliases. No behavior change; equip-by-name keeps full suffixed
matching via the decorated location form.
2026-07-15 19:12:56 -05:00
Brues 653f8db3d7 Reconcile /startattack against real action-bar state, not cached flag
The autoAttack flag could drift stale-true (optimistic set after
AttackTarget, or a target dying without PLAYER_LEAVE_COMBAT), making
/startattack skip the attack until the target was dropped and reselected.
The old fallback called the overridden IsCurrentAction, which just echoes
the cached flag for the attack slot, so it never detected drift. Use the
original Hooks.IsCurrentAction as ground truth and sync the flag to it.
2026-07-04 19:38:37 -05:00
Brues 86fb0254e6 Show autoattack icon and glow 2026-06-30 12:34:34 -05:00
Brues fc640abb07 Render known-spell action tooltips via spellbook slot, not spellID
GameTooltip.SetAction used GameTooltip:SetSpellByID for spell actions, which
renders the static DBC tooltip. For spells we resolved from the player's own
spellbook, use GameTooltip:SetSpell(spellSlot, bookType) instead so the
tooltip shows live player-accurate data (mana cost, cooldown, range coloring,
reagent counts). spellSlot/bookType/id all come from the same spellbook index
entry, so it renders the identical spell and rank. SetSpellByID remains as a
defensive fallback for entries without a slot. Applied to both the direct and
nested-macro tooltip paths.
2026-06-23 12:41:17 -05:00
Brues 3384765faa dont need pcall around registering events 2026-06-22 13:04:05 -05:00
25 changed files with 1273 additions and 1520 deletions
+181 -8
View File
@@ -52,13 +52,13 @@ end
-- C_UnitAuras
--------------------------------------------------------------------------------
-- Scan a unit's auras (indexFn = C_UnitAuras.GetBuffDataByIndex or
-- GetDebuffDataByIndex) for one matching the dispel type. Vanilla descriptors
-- hold 32 helpful / 16 harmful slots; cap as a backstop.
local function scanDispel(indexFn, unit, dispelType, wantAny)
-- Scan one aura range of `unit` (filter = "HELPFUL" or "HARMFUL") for an aura
-- matching the dispel type. The filtered index self-terminates at the end of the
-- range (nil); 48 is a backstop over vanilla's 32 helpful / 16 harmful slots.
local function scanDispel(unit, filter, dispelType, wantAny)
local i = 1
while i <= 48 do
local data = indexFn(unit, i)
local data = C_UnitAuras.GetAuraDataByIndex(unit, i, filter)
if not data then return false end
local dn = data.dispelName
if dn and dn ~= "" and (wantAny or dn == dispelType) then
@@ -76,10 +76,29 @@ end
-- false -> scan debuffs (defensive cleanse, default)
function API.UnitHasDispelType(unit, dispelType, helpful)
if not unit or not UnitExists(unit) then return false end
local indexFn = helpful and C_UnitAuras.GetBuffDataByIndex
or C_UnitAuras.GetDebuffDataByIndex
local filter = helpful and "HELPFUL" or "HARMFUL"
local wantAny = (dispelType == nil or dispelType == "any")
return scanDispel(indexFn, unit, dispelType, wantAny)
return scanDispel(unit, filter, dispelType, wantAny)
end
-- First matching aura on `unit` by spellID, or nil. With no filter it walks the
-- whole aura array (helpful then harmful), so it finds a debuff even when it has
-- overflowed into an NPC's buff slots -- no 16+32 slot scan, no UnitIsPlayer
-- gate. filter ("HELPFUL"/"HARMFUL") restricts the search. Returns the modern
-- AuraData (spellId, name, applications, duration, expirationTime, dispelName, ...).
function API.GetUnitAuraBySpellID(unit, spellID, filter)
if not unit or not spellID then return nil end
return C_UnitAuras.GetUnitAuraBySpellID(unit, spellID, filter)
end
-- First matching aura on `unit` by spell NAME, or nil. Same whole-array search as
-- GetUnitAuraBySpellID; the name is case-sensitive and locale-resolved, so pass it
-- in the client's locale (what C_Spell.GetSpellName returns). filter
-- ("HELPFUL"/"HARMFUL") restricts the search. Prefer the by-ID variant for
-- portability where a spellID is known.
function API.GetAuraDataBySpellName(unit, spellName, filter)
if not unit or not spellName or spellName == "" then return nil end
return C_UnitAuras.GetAuraDataBySpellName(unit, spellName, filter)
end
--------------------------------------------------------------------------------
@@ -155,6 +174,76 @@ function API.GetItemSetInfo(setID)
return C_Item.GetItemSetInfo(setID)
end
--------------------------------------------------------------------------------
-- Equipment Set
--------------------------------------------------------------------------------
-- True if the saved equipment set named `name` is currently equipped -- every
-- resolvable item in its target slot (a missing bank-stored piece doesn't
-- disqualify, matching C_EquipmentSet.GetEquipmentSetInfo's isEquipped). Name is
-- an exact, case-sensitive match per GetEquipmentSetID. Returns false for an
-- unknown name or a client without the EquipmentSet API. Powers [equipset]/
-- [noequipset]; the swap side is the reclaimed /equipset command.
function API.IsEquipmentSetEquipped(name)
if not name or name == "" then return false end
local setID = C_EquipmentSet.GetEquipmentSetID(name)
if not setID then return false end
local _, _, _, isEquipped = C_EquipmentSet.GetEquipmentSetInfo(setID)
return isEquipped and true or false
end
--------------------------------------------------------------------------------
-- Weapon Enchant
--------------------------------------------------------------------------------
-- Temporary weapon-enchant state for a slot: "mh" (main), "oh" (off), or
-- "ranged". Returns hasEnchant, expirationMs, charges, enchantID. The enchantID
-- comes from ClassicAPI's modern C_Item.GetWeaponEnchantInfo 12-tuple (the
-- vanilla global omits it), so [mhenchant] can tell WHICH imbue is applied, not
-- just that one exists. Falls back to the vanilla 6-tuple global (enchantID nil,
-- no ranged slot) when the C_Item version is unavailable.
function API.GetWeaponEnchant(slot)
local hasM, mExp, mChg, mID, hasO, oExp, oChg, oID, hasR, rExp, rChg, rID =
C_Item.GetWeaponEnchantInfo()
if slot == "oh" then
return hasO, oExp, oChg, oID
elseif slot == "ranged" then
return hasR, rExp, rChg, rID
end
return hasM, mExp, mChg, mID
end
-- Localized name of an item-enchant ID (poison/oil/sharpening stone/permanent),
-- read straight from SpellItemEnchantment.dbc via ClassicAPI -- or nil for an
-- unknown id / a client without C_Item.GetEnchantInfo. Lets [mhenchant:Name]
-- resolve the applied enchant's name without scraping the weapon tooltip.
function API.GetEnchantName(enchantID)
if not enchantID or enchantID == 0 then return nil end
local info = C_Item.GetEnchantInfo(enchantID)
return info and info.name or nil
end
--------------------------------------------------------------------------------
-- Loss Of Control
--------------------------------------------------------------------------------
-- Locked spell-school mask from an active SCHOOL_INTERRUPT (Counterspell / Kick /
-- Pummel / Earth Shock lockout) on the player, or 0 when not kicked. Read from
-- C_LossOfControl, which synthesizes the lockout from the server's own
-- SMSG_SPELL_COOLDOWN packet -- a state no debuff scan can see. Also returns the
-- seconds remaining (nil if ClassicAPI didn't observe the applying cast). Returns
-- 0 for a client without C_LossOfControl. Player-only (vanilla LoC is local-only).
function API.GetSchoolLockout()
local n = C_LossOfControl.GetActiveLossOfControlDataCount() or 0
for i = 1, n do
local d = C_LossOfControl.GetActiveLossOfControlData(i)
if d and d.locType == "SCHOOL_INTERRUPT" then
return d.lockoutSchool or 0, d.timeRemaining
end
end
return 0, nil
end
--------------------------------------------------------------------------------
-- Spell
--------------------------------------------------------------------------------
@@ -168,6 +257,67 @@ function API.GetSpellMechanicByID(spellID)
return C_Spell.GetSpellMechanicByID(spellID)
end
-- Per-effect SpellMechanic ids (Spell.dbc EffectMechanic[3]) as {m1, m2, m3},
-- or nil for an invalid spell / 0 for an effect with no mechanic. Complements
-- GetSpellMechanicByID, which only reads the spell-level Mechanic field: vanilla
-- stores some mechanics on an effect instead (e.g. Rake's bleed is effect-level,
-- so GetSpellMechanicByID returns 0 but this returns {0,15,0}). Nil-guarded so an
-- older ClassicAPI build without the function degrades gracefully.
function API.GetSpellEffectMechanics(spellID)
return C_Spell.GetSpellEffectMechanics(spellID)
end
-- Flat spell-damage bonus (spell power) for a magic school, as a number.
-- school is 1-based: 1=Physical, 2=Holy, 3=Fire, 4=Nature, 5=Frost, 6=Shadow,
-- 7=Arcane. Reads the same client field nampower's GetSpellPower does -- exact,
-- with gear/enchants/buffs/talents/set bonuses baked in.
function API.GetSpellBonusDamage(school)
return GetSpellBonusDamage(school)
end
-- Flat healing bonus (+healing), as a number. Vanilla has no healing-done field,
-- so ClassicAPI derives it from gear/enchant/buff MOD_HEALING_DONE plus
-- stat-conversion talents (e.g. Spiritual Guidance) -- exact, not a holy-damage
-- proxy.
function API.GetSpellBonusHealing()
return GetSpellBonusHealing()
end
--------------------------------------------------------------------------------
-- Unit Health
--------------------------------------------------------------------------------
-- Health deficit (max - current) for `unit` in one call. Falls back to
-- UnitHealthMax - UnitHealth without ClassicAPI.
API.UnitHealthMissing = UnitHealthMissing or function(unit)
return (UnitHealthMax(unit) or 0) - (UnitHealth(unit) or 0)
end
--------------------------------------------------------------------------------
-- Unit Power
--------------------------------------------------------------------------------
-- Current power for a specific Enum.PowerType (0=Mana, 1=Rage, 2=Focus,
-- 3=Energy, 4=Happiness), or the unit's primary power when powerType is omitted.
-- Display-divided (rage reads 0..100). Falls back to UnitMana without ClassicAPI.
API.UnitPower = UnitPower or function(unit, powerType)
return UnitMana(unit)
end
API.UnitPowerMax = UnitPowerMax or function(unit, powerType)
return UnitManaMax(unit)
end
-- Power deficit (max - current) for the type / primary power, in one call.
API.UnitPowerMissing = UnitPowerMissing or function(unit, powerType)
return (UnitManaMax(unit) or 0) - (UnitMana(unit) or 0)
end
-- Unit's primary power type as an integer (0=Mana .. 4=Happiness).
function API.UnitPowerType(unit)
return UnitPowerType(unit)
end
--------------------------------------------------------------------------------
-- State
--------------------------------------------------------------------------------
@@ -182,6 +332,29 @@ function API.IsSwimming()
return IsSwimming() and true or false
end
-- True if the player is currently mounted.
function API.IsMounted()
return IsMounted() and true or false
end
-- True if the player is under a WMO roof (building, cave, instance interior).
-- Live engine geometry query, not zone-based; nil (-> false) pre-world.
function API.IsIndoors()
return IsIndoors() and true or false
end
-- True if the player is outdoors (open sky / not inside a WMO interior).
-- Exact complement of IsIndoors for a resolvable player.
function API.IsOutdoors()
return IsOutdoors() and true or false
end
-- Player's stand state: 0 = standing, non-zero = sitting/sleeping/kneeling/etc.
-- (see UnitStandState). Player-only.
function API.GetPlayerStandState()
return UnitStandState("player") or 0
end
--------------------------------------------------------------------------------
-- Cursor
--------------------------------------------------------------------------------
+40 -67
View File
@@ -32,6 +32,35 @@ CleveRoids.lastRakeCast = CleveRoids.lastRakeCast or {
timestamp = 0
}
-- Family membership without hardcoded rank lists. C_Spell.GetSpellName resolves
-- ANY spellID from the client's Spell.dbc -- every rank (so no enumeration or
-- spellbook scan) and TWoW's custom spells alike -- so "is this spellID a Rip?"
-- is just a name match against one seed rank. seedNameCache memoizes the seed's
-- localized name (locale-safe: derived from the ID, not a hardcoded string; a
-- seed absent from the DBC, e.g. a TWoW spell on a stock client, resolves to nil
-- and the set simply never matches -- which is correct, that spell can't be cast).
local seedNameCache = {}
local function SeedName(seedID)
local n = seedNameCache[seedID]
if n == nil then
n = C_Spell.GetSpellName(seedID) or false
seedNameCache[seedID] = n
end
return n or nil
end
-- Stand-in for a hardcoded {spellID=true,...} rank set: t[spellID] is true iff
-- spellID is any rank of the family seeded by seedID. Same read shape as the old
-- tables, so every membership consumer (`X and X[id]`) keeps working unchanged.
-- Only valid for membership tests -- these are not iterable (pairs() sees empty).
local function RankSet(seedID)
return setmetatable({}, { __index = function(_, spellID)
if type(spellID) ~= "number" then return nil end
local name = SeedName(seedID)
return (name and C_Spell.GetSpellName(spellID) == name) and true or nil
end })
end
-- Define spells that scale with combo points by SPELL ID and their duration formulas
-- Duration = base + (combo_points - 1) * increment
CleveRoids.ComboScalingSpellsByID = {
@@ -70,32 +99,9 @@ CleveRoids.FerociousBiteSpellIDs = {
[31018] = true, -- Rank 6
}
-- Rip spell IDs (for Carnage talent - refreshed when Carnage procs)
CleveRoids.RipSpellIDs = {
[1079] = true, -- Rank 1
[9492] = true, -- Rank 2
[9493] = true, -- Rank 3
[9752] = true, -- Rank 4
[9894] = true, -- Rank 5
[9896] = true, -- Rank 6
}
-- Rake spell IDs (for Carnage talent - refreshed when Carnage procs)
CleveRoids.RakeSpellIDs = {
[1822] = true, -- Rank 1
[1823] = true, -- Rank 2
[1824] = true, -- Rank 3
[9904] = true, -- Rank 4
}
-- Pounce Bleed spell IDs (for immunity detection - bleed portion of Pounce)
-- Note: Pounce (cast) TRIGGERS a separate Pounce Bleed spell with different IDs
-- Cast IDs: 9005, 9823, 9827 → Trigger Bleed IDs: 9007, 9824, 9826
CleveRoids.PounceBleedSpellIDs = {
[9007] = true, -- Rank 1 (triggered by Pounce 9005)
[9824] = true, -- Rank 2 (triggered by Pounce 9823)
[9826] = true, -- Rank 3 (triggered by Pounce 9827)
}
-- Rip / Rake families (for Carnage talent). Seeded by Rank 1; matches every rank.
CleveRoids.RipSpellIDs = RankSet(1079)
CleveRoids.RakeSpellIDs = RankSet(1822)
-- Combined table for all bleed spells that need immunity detection
-- Used when checking if a cast bleed failed to apply (indicates bleed immunity)
@@ -132,14 +138,9 @@ CleveRoids.PounceToBleedMapping = {
-- When Molten Blast hits, it refreshes Flame Shock duration on the target
-- Detection: Monitor combat log for Molten Blast damage, then refresh Flame Shock
-- =============================================================================
CleveRoids.MoltenBlastSpellIDs = {
[36916] = true, -- Rank 1
[36917] = true, -- Rank 2
[36918] = true, -- Rank 3
[36919] = true, -- Rank 4
[36920] = true, -- Rank 5
[36921] = true, -- Rank 6
}
-- TWoW custom; seed resolves on the TWoW client (nil/never-matches on stock,
-- where Molten Blast can't be cast anyway).
CleveRoids.MoltenBlastSpellIDs = RankSet(36916)
CleveRoids.FlameShockSpellIDs = {
[8050] = true, -- Rank 1
@@ -154,12 +155,7 @@ CleveRoids.FlameShockSpellIDs = {
-- WARLOCK: Conflagrate → Immolate Duration Reduction
-- When Conflagrate is cast, it reduces Immolate duration by 3 seconds
-- =============================================================================
CleveRoids.ConflagrateSpellIDs = {
[17962] = true, -- Rank 1
[18930] = true, -- Rank 2
[18931] = true, -- Rank 3
[18932] = true, -- Rank 4
}
CleveRoids.ConflagrateSpellIDs = RankSet(17962)
CleveRoids.ImmolateSpellIDs = {
[348] = true, -- Rank 1
@@ -177,11 +173,8 @@ CleveRoids.ImmolateSpellIDs = {
-- Channeled spell that accelerates DoT tick rate by 30% while channeling
-- Complex tracking: debuff expires 30% faster while Dark Harvest is active
-- =============================================================================
CleveRoids.DarkHarvestSpellIDs = {
[52550] = true, -- Rank 1
[52551] = true, -- Rank 2
[52552] = true, -- Rank 3
}
-- TWoW custom (see MoltenBlast note).
CleveRoids.DarkHarvestSpellIDs = RankSet(52550)
-- =============================================================================
-- DRUID: Rake Debuff Cap Boss Whitelist
@@ -489,7 +482,7 @@ function CleveRoids.TrackComboPointCastByID(spellID, targetGUID)
end
else
-- Second, check if name-based tracking has recent data for this spell
local spellName = GetSpellRecField(spellID, "name")
local spellName = C_Spell.GetSpellName(spellID)
if spellName then
-- Remove rank info for comparison
local baseName = CleveRoids.StripRank(spellName)
@@ -548,26 +541,6 @@ function CleveRoids.TrackComboPointCastByID(spellID, targetGUID)
return duration
end
-- API function to get last tracked combo points for a spell
function CleveRoids.GetLastComboPointsForSpell(spellName)
if CleveRoids.ComboPointTracking[spellName] then
return CleveRoids.ComboPointTracking[spellName].combo_points
elseif CleveRoids.spell_tracking[spellName] then
return CleveRoids.spell_tracking[spellName].last_combo_points
end
return nil
end
-- API function to get last calculated duration for a spell
function CleveRoids.GetLastDurationForSpell(spellName)
if CleveRoids.ComboPointTracking[spellName] then
return CleveRoids.ComboPointTracking[spellName].duration
elseif CleveRoids.spell_tracking[spellName] then
return CleveRoids.spell_tracking[spellName].last_duration
end
return nil
end
-- Utility function to display current combo tracking info
function CleveRoids.ShowComboTracking()
CleveRoids.Print("=== Combo Point Tracking ===")
@@ -683,7 +656,7 @@ if _G.UseAction then
local spellName = nil
if actionType == "SPELL" and actionID then
spellName = GetSpellRecField(actionID, "name")
spellName = C_Spell.GetSpellName(actionID)
end
if currentCP and currentCP > 0 then
+25 -19
View File
@@ -2,9 +2,10 @@ local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
local Extension = CleveRoids.RegisterExtension("Compatibility_pfUI")
Extension.RegisterEvent("ADDON_LOADED", "ADDON_LOADED")
Extension.RegisterEvent("PLAYER_LOGIN", "PLAYER_LOGIN")
Extension.Debug = false
-- pfUI-loaded and player-login handlers are wired via ClassicAPI's EventUtil at
-- the bottom of the file (ContinueOnAddOnLoaded fires immediately if pfUI already
-- loaded, so no separate "we missed pfUI's ADDON_LOADED" fallback is needed).
-- Track pfUI state
Extension.pfUILoaded = false
@@ -99,7 +100,7 @@ local function GetCarnageOverride(effect)
end
for spellID, override in pairs(CleveRoids.carnageDurationOverrides) do
local spellName = GetSpellRecField(spellID, "name")
local spellName = C_Spell.GetSpellName(spellID)
if spellName then
local baseName = CleveRoids.StripRank(spellName)
if baseName == effect and override.timestamp and (GetTime() - override.timestamp) < 5 then
@@ -209,7 +210,7 @@ function Extension.HookPfUILibdebuff()
for spellID, rec in pairs(CleveRoids.libdebuff.objects[unitGUID]) do
if rec and rec.start and rec.duration then
-- Get spell name for this ID
local spellName = GetSpellRecField(spellID, "name")
local spellName = C_Spell.GetSpellName(spellID)
if spellName then
local baseName = CleveRoids.StripRank(spellName)
if baseName == effect then
@@ -380,7 +381,7 @@ function Extension.SyncComboDurationToPfUI(guid, spellID, duration)
end
-- Get spell name from spell ID
local spellName = GetSpellRecField(spellID, "name")
local spellName = C_Spell.GetSpellName(spellID)
if not spellName then
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[pfUI Sync]|r Could not find spell name for ID " .. spellID)
@@ -726,23 +727,22 @@ function Extension.OnLoad()
SLASH_PFUICD1 = "/pfuicd"
end
function Extension.ADDON_LOADED()
-- Check if pfUI just loaded AND the global actually exists
-- (another addon could be named "pfUI" without being the real UI framework)
if arg1 == "pfUI" and pfUI then
Extension.pfUILoaded = true
-- pfUI modules load after ADDON_LOADED, so schedule a check
if CleveRoids.ScheduleTimer then
CleveRoids.ScheduleTimer(function()
Extension.SetupCompatibility()
end, 0.5)
end
-- Fires once pfUI has loaded (immediately if it loaded before us, via EventUtil).
function Extension.OnPfUILoaded()
-- Guard: only the real pfUI framework sets this global (another addon could be
-- named "pfUI" without being the UI framework).
if not pfUI then return end
Extension.pfUILoaded = true
-- pfUI's submodules initialize after its ADDON_LOADED, so defer the setup.
if CleveRoids.ScheduleTimer then
CleveRoids.ScheduleTimer(function()
Extension.SetupCompatibility()
end, 0.5)
end
end
function Extension.PLAYER_LOGIN()
-- If pfUI loaded before SCRM (alphabetical order), ADDON_LOADED for pfUI was missed.
-- Re-run InitPfUIIntegration here to ensure lib.objects is linked correctly.
function Extension.OnPlayerLogin()
-- Ensure lib.objects is linked correctly (InitPfUIIntegration is idempotent).
if pfUI and not CleveRoids.hasPfUI76 then
local lib = CleveRoids.libdebuff
if lib and lib.InitPfUIIntegration then
@@ -797,4 +797,10 @@ if not CleveRoids.ScheduleTimer then
end
end
-- Wire handlers via ClassicAPI EventUtil (fires immediately if the event already
-- happened, so load order relative to pfUI no longer matters). Registered here,
-- after the handlers are defined, since ContinueOnAddOnLoaded may fire inline.
EventUtil.ContinueOnAddOnLoaded("pfUI", Extension.OnPfUILoaded)
EventUtil.ContinueOnPlayerLogin(Extension.OnPlayerLogin)
_G["CleveRoids"] = CleveRoids
+350 -175
View File
@@ -55,15 +55,14 @@ function CleveRoids.GetCachedPlayerHealthPercent()
end
function CleveRoids.GetCachedPlayerPowerPercent()
local API = CleveRoids.NampowerAPI
local power = API and API.GetUnitPower and API.GetUnitPower("player") or UnitMana("player")
local max = API and API.GetUnitMaxPower and API.GetUnitMaxPower("player") or UnitManaMax("player")
local API = CleveRoids.ClassicAPI
local power = API.UnitPower("player")
local max = API.UnitPowerMax("player")
return max > 0 and (100 * power / max) or 0
end
function CleveRoids.GetCachedPlayerPower()
local API = CleveRoids.NampowerAPI
return API and API.GetUnitPower and API.GetUnitPower("player") or UnitMana("player")
return CleveRoids.ClassicAPI.UnitPower("player")
end
function CleveRoids.GetCachedTargetHealthPercent()
@@ -105,7 +104,7 @@ local function BuildSpellNameCache()
if lib.personalDebuffs then
for sid, _ in pairs(lib.personalDebuffs) do
local name = GetSpellRecField(sid, "name")
local name = C_Spell.GetSpellName(sid)
if name then
name = CleveRoids.StripRank(name)
if not _spellNameToIDs[name] then
@@ -118,7 +117,7 @@ local function BuildSpellNameCache()
if lib.sharedDebuffs then
for sid, _ in pairs(lib.sharedDebuffs) do
local name = GetSpellRecField(sid, "name")
local name = C_Spell.GetSpellName(sid)
if name then
name = CleveRoids.StripRank(name)
if not _spellNameToIDs[name] then
@@ -163,7 +162,7 @@ local function GetSpellIDForRank(baseName, rankNum)
if not matchIDs then return nil end
local targetRank = "Rank " .. rankNum
for _, sid in ipairs(matchIDs) do
local rank = GetSpellRecField and GetSpellRecField(sid, "rank")
local rank = C_Spell.GetSpellSubtext(sid)
if rank and rank == targetRank then
return sid
end
@@ -273,24 +272,17 @@ local function BuildEquipmentCache()
end
end
-- Fallback: manual slot enumeration
-- 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 link = GetInventoryItemLink("player", slot)
if link then
local _, _, id = string_find(link, "item:(%d+)")
local _, _, nameInBrackets = string_find(link, "%[(.+)%]")
if id then
_equippedItemIDs[slot] = tonumber(id)
end
if nameInBrackets then
_equippedItemNames[slot] = string_lower(nameInBrackets)
elseif id then
-- Fallback: resolve via GetItemInfo
local itemName = GetItemInfo(tonumber(id))
if itemName then
_equippedItemNames[slot] = string_lower(itemName)
end
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
@@ -350,8 +342,7 @@ function CleveRoids.FindItemLocation(item)
if numericItem then
-- Check if it's an equipment slot (1-19)
if numericItem >= 1 and numericItem <= 19 then
local link = GetInventoryItemLink("player", numericItem)
if link then
if GetInventoryItemID("player", numericItem) then
return { type = "inventory", inventoryID = numericItem }
end
return nil
@@ -452,24 +443,26 @@ local stat_checks = {
attackpower = function() local base, pos, neg = UnitAttackPower("player"); return base + pos + neg end,
rap = function() local base, pos, neg = UnitRangedAttackPower("player"); return base + pos + neg end,
rangedattackpower = function() local base, pos, neg = UnitRangedAttackPower("player"); return base + pos + neg end,
healing = function() local _, h = CleveRoids.NampowerAPI.GetSpellPower(); return h or 0 end,
healingpower = function() local _, h = CleveRoids.NampowerAPI.GetSpellPower(); return h or 0 end,
healing = function() return CleveRoids.ClassicAPI.GetSpellBonusHealing() or 0 end,
healingpower = function() return CleveRoids.ClassicAPI.GetSpellBonusHealing() or 0 end,
-- Bonus Spell Damage by School (Nampower v2.31+ GetSpellPower)
-- GetSpellPower() returns: physical, holy, fire, nature, frost, shadow, arcane
arcane_power = function() return select(7, CleveRoids.NampowerAPI.GetSpellPower()) or 0 end,
fire_power = function() return select(3, CleveRoids.NampowerAPI.GetSpellPower()) or 0 end,
frost_power = function() return select(5, CleveRoids.NampowerAPI.GetSpellPower()) or 0 end,
nature_power = function() return select(4, CleveRoids.NampowerAPI.GetSpellPower()) or 0 end,
shadow_power = function() return select(6, CleveRoids.NampowerAPI.GetSpellPower()) or 0 end,
-- Bonus Spell Damage by School (ClassicAPI GetSpellBonusDamage)
-- school: 1=Physical, 2=Holy, 3=Fire, 4=Nature, 5=Frost, 6=Shadow, 7=Arcane
arcane_power = function() return CleveRoids.ClassicAPI.GetSpellBonusDamage(7) or 0 end,
fire_power = function() return CleveRoids.ClassicAPI.GetSpellBonusDamage(3) or 0 end,
frost_power = function() return CleveRoids.ClassicAPI.GetSpellBonusDamage(5) or 0 end,
nature_power = function() return CleveRoids.ClassicAPI.GetSpellBonusDamage(4) or 0 end,
shadow_power = function() return CleveRoids.ClassicAPI.GetSpellBonusDamage(6) or 0 end,
-- Highest spell power across all schools
spell_power = function()
local p, h, fi, n, fr, s, a = CleveRoids.NampowerAPI.GetSpellPower()
if p then
return math.max(p, h, fi, n, fr, s, a)
local API = CleveRoids.ClassicAPI
local best = 0
for s = 1, 7 do
local v = API.GetSpellBonusDamage(s) or 0
if v > best then best = v end
end
return 0
return best
end,
-- Defensive Stats
@@ -924,7 +917,7 @@ function CleveRoids.GetAllCasterAuraTimeRemaining(targetGuid, spellId)
local targetData, isPfUI = CleveRoids.GetAuraTrackingData(targetGuid)
if not targetData then return nil end
local spellName = GetSpellRecField and GetSpellRecField(spellId, "name")
local spellName = C_Spell.GetSpellName(spellId)
if not spellName then return nil end
local casters = targetData[spellName]
@@ -968,7 +961,7 @@ function CleveRoids.FindAllCasterAuraByName(targetGuid, searchName)
-- Resolve spell ID to name for direct lookup
local searchID = tonumber(searchName)
if searchID then
local resolvedName = GetSpellRecField and GetSpellRecField(searchID, "name")
local resolvedName = C_Spell.GetSpellName(searchID)
if not resolvedName then return nil, nil end
searchName = resolvedName
end
@@ -1089,7 +1082,7 @@ local function OnAutoAttackOther(attackerGuid, targetGuid, totalDamage, hitInfo,
rec.start = GetTime()
if CleveRoids.debug then
local spellName = GetSpellRecField and GetSpellRecField(spellID, "name") or "Unknown"
local spellName = C_Spell.GetSpellName(spellID) or "Unknown"
local baseName = CleveRoids.StripRank(spellName) or "Unknown"
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ffaa[Judgement Refresh]|r Refreshed %s (ID:%d) on melee hit - new duration: %ds",
@@ -1099,7 +1092,7 @@ local function OnAutoAttackOther(attackerGuid, targetGuid, totalDamage, hitInfo,
-- Sync to pfUI if loaded (pre-7.6 only)
if not CleveRoids.hasPfUI76 and pfUI and pfUI.api and pfUI.api.libdebuff then
local spellName = GetSpellRecField and GetSpellRecField(spellID, "name") or nil
local spellName = C_Spell.GetSpellName(spellID) or nil
local baseName = CleveRoids.StripRank(spellName)
local targetName = (lib.guidToName and lib.guidToName[normalizedTarget]) or UnitName("target")
local targetLevel = UnitLevel("target") or 0
@@ -1244,7 +1237,7 @@ local function OnAuraCastSelf(spellId, casterGuid, targetGuid, effect, effectAur
-- Use the isBuffNotDebuff result determined above (avoids redundant slot scanning)
local lib = CleveRoids.libdebuff
if isBuffNotDebuff and spellId and durationMs and durationMs > 0 and lib and not lib.hasPfUIEnhanced then
local spellName = GetSpellRecField and GetSpellRecField(spellId, "name")
local spellName = C_Spell.GetSpellName(spellId)
if spellName then
local playerGuid = CleveRoids.GetGUID("player")
if playerGuid then
@@ -1277,7 +1270,7 @@ local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAu
-- When pfUI enhanced is active, pfUI writes to pfUI.libdebuff_all_auras with
-- full downrank protection — we read from that table via GetAuraTrackingData().
if spellId and durationMs and durationMs > 0 then
local spellName = GetSpellRecField and GetSpellRecField(spellId, "name")
local spellName = C_Spell.GetSpellName(spellId)
if spellName and not CleveRoids.hasPfUI76 then
CleveRoids._allCasterAuraDirty = true
if not CleveRoids.AllCasterAuraTracking[targetGuid] then
@@ -1335,7 +1328,7 @@ local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAu
-- (AURA_CAST_ON_OTHER fires for both buffs and debuffs; BUFF_ADDED_OTHER confirms buff)
local lib = CleveRoids.libdebuff
if lib and not lib.hasPfUIEnhanced then
local spellNameForPending = GetSpellRecField and GetSpellRecField(spellId, "name")
local spellNameForPending = C_Spell.GetSpellName(spellId)
if spellNameForPending then
local normTargetGuid = CleveRoids.NormalizeGUID(targetGuid)
if normTargetGuid then
@@ -1480,7 +1473,7 @@ autoAttackFrame:SetScript("OnEvent", function()
if spellId and spellId > 0 and durationMs and durationMs > 0 then
local playerGUID = CleveRoids.GetGUID("player")
local durSpellName = GetSpellRecField and GetSpellRecField(spellId, "name")
local durSpellName = C_Spell.GetSpellName(spellId)
if playerGUID and durSpellName and not CleveRoids.hasPfUI76 then
CleveRoids._allCasterAuraDirty = true
if not CleveRoids.AllCasterAuraTracking[playerGUID] then
@@ -1532,7 +1525,7 @@ autoAttackFrame:SetScript("OnEvent", function()
local state = arg7
if state == 2 then return end -- Stack change, not full removal
if guid and spellId and CleveRoids.AllCasterAuraTracking[guid] then
local removedName = GetSpellRecField and GetSpellRecField(spellId, "name")
local removedName = C_Spell.GetSpellName(spellId)
if removedName and CleveRoids.AllCasterAuraTracking[guid][removedName] then
CleveRoids.AllCasterAuraTracking[guid][removedName] = nil
if not next(CleveRoids.AllCasterAuraTracking[guid]) then
@@ -1888,7 +1881,7 @@ local function IsPendingDebuffCast(spellName, targetUnit)
if arr then
for _, pending in pairs(arr) do
if pending and pending.targetGUID == targetGuid and pending.spellID then
local pendingName = GetSpellRecField and GetSpellRecField(pending.spellID, "name")
local pendingName = C_Spell.GetSpellName(pending.spellID)
if pendingName then
local normalizedPending = NormalizeSpellNameForComparison(pendingName)
if normalizedPending == normalizedCheck then
@@ -2026,7 +2019,7 @@ function CleveRoids.CancelAura(auraName)
if aura_ix == -1 then break end
local bid = GetPlayerBuffID(aura_ix)
bid = (bid < -1) and (bid + 65536) or bid
if string.lower(GetSpellRecField(bid, "name") or "") == auraName then
if string.lower(C_Spell.GetSpellName(bid) or "") == auraName then
C_Spell.CancelSpellByID(bid)
return true
end
@@ -2039,7 +2032,7 @@ function CleveRoids.CancelAura(auraName)
for slot = 0, 31 do
local spellId = _G.GetPlayerAuraDuration(slot)
if spellId and spellId > 0 then
local name = GetSpellRecField(spellId, "name")
local name = C_Spell.GetSpellName(spellId)
if name and string.lower(name) == auraName then
C_Spell.CancelSpellByID(spellId)
return true
@@ -2055,7 +2048,7 @@ function CleveRoids.CancelAura(auraName)
if entry.durationSec and entry.durationSec > 0 and elapsed > entry.durationSec then
CleveRoids.OverflowBuffs[spellId] = nil
else
local name = GetSpellRecField(spellId, "name")
local name = C_Spell.GetSpellName(spellId)
if name and string.lower(name) == auraName then
C_Spell.CancelSpellByID(spellId)
CleveRoids.OverflowBuffs[spellId] = nil
@@ -2960,11 +2953,8 @@ function CleveRoids.CountEnemiesMatching(checkFunc)
tryUnit("targettarget")
tryUnit("targettargettarget")
tryUnit("pettarget")
if pfUI and pfUI.uf and pfUI.uf.focus and pfUI.uf.focus.label and pfUI.uf.focus.id then
local focusUnit = pfUI.uf.focus.label .. pfUI.uf.focus.id
tryUnit(focusUnit)
tryUnit(focusUnit .. "target")
end
tryUnit("focus")
tryUnit("focustarget")
for i = 1, 4 do
tryUnit("party" .. i .. "target")
end
@@ -2974,22 +2964,8 @@ function CleveRoids.CountEnemiesMatching(checkFunc)
end
end
-- 3. Nameplate scan: visible nameplates give live GUIDs without target switching.
-- ClassicAPI's C_NamePlate.GetNamePlateGUIDs() lists every unit with an
-- allocated nameplate (including default vanilla nameplates), replacing the
-- old WorldFrame child-walk + frame:GetName(1) GUID-extraction.
local plateGuids = CleveRoids.ClassicAPI.GetNamePlateGUIDs()
if plateGuids then
for i = 1, table.getn(plateGuids) do
local guid = plateGuids[i]
if guid and not checked[guid] and UnitExists(guid) and UnitCanAttack("player", guid) then
checked[guid] = true
CleveRoids.knownEnemyGuids[guid] = true
if checkFunc(guid) then
count = count + 1
end
end
end
for i = 1, 40 do
tryUnit("nameplate"..i)
end
-- During tooltip evaluation, skip known-enemy cache iteration.
@@ -3305,9 +3281,9 @@ end
-- returns: True or false
function CleveRoids.ValidatePower(unit, operator, amount)
if not unit or not operator or not amount then return false end
local API = CleveRoids.NampowerAPI
local power = API and API.GetUnitPower and API.GetUnitPower(unit) or UnitMana(unit)
local maxPower = API and API.GetUnitMaxPower and API.GetUnitMaxPower(unit) or UnitManaMax(unit)
local API = CleveRoids.ClassicAPI
local power = API.UnitPower(unit)
local maxPower = API.UnitPowerMax(unit)
local powerPercent = maxPower > 0 and (100 * power / maxPower) or 0
if CleveRoids.operators[operator] then
@@ -3317,6 +3293,26 @@ function CleveRoids.ValidatePower(unit, operator, amount)
return false
end
-- Validates a SPECIFIC power slot (Enum.PowerType: 0=Mana, 1=Rage, 3=Energy) as a
-- percentage, regardless of the unit's primary power -- so [mana]/[energy]/[rage]
-- read the right pool cross-form (druid mana in cat) and cross-unit (@target mana).
-- A unit with no such pool (max <= 0) fails rather than reading as 0% (avoids a
-- rageless caster satisfying [rage:<10]).
function CleveRoids.ValidateTypedPower(unit, powerType, operator, amount)
if not unit or not operator or not amount then return false end
if not UnitExists(unit) then return false end
local API = CleveRoids.ClassicAPI
local maxPower = API.UnitPowerMax(unit, powerType)
if not maxPower or maxPower <= 0 then return false end
local powerPercent = 100 * API.UnitPower(unit, powerType) / maxPower
if CleveRoids.operators[operator] then
return CleveRoids.comparators[operator](powerPercent, amount)
end
return false
end
-- Checks whether or not the given unit has current power vs the given amount
-- unit: The unit we're checking
-- operator: valid comparitive operator symbol
@@ -3324,8 +3320,7 @@ end
-- returns: True or false
function CleveRoids.ValidateRawPower(unit, operator, amount)
if not unit or not operator or not amount then return false end
local API = CleveRoids.NampowerAPI
local power = API and API.GetUnitPower and API.GetUnitPower(unit) or UnitMana(unit)
local power = CleveRoids.ClassicAPI.UnitPower(unit)
if power and CleveRoids.operators[operator] then
return CleveRoids.comparators[operator](power, amount)
@@ -3334,23 +3329,15 @@ function CleveRoids.ValidateRawPower(unit, operator, amount)
return false
end
-- Raw caster-form mana for druids (SuperWoW: 2nd return of UnitMana)
-- Raw caster-form mana for druids. Read the mana power slot directly
-- (0 = Enum.PowerType.Mana) so it works while shapeshifted, when the druid's
-- primary power is rage/energy -- no SuperWoW 2nd-return-of-UnitMana needed.
function CleveRoids.ValidateDruidRawMana(unit, operator, amount)
unit = unit or "player"
if not operator or amount == nil then return false end
if (CleveRoids.playerClass ~= "DRUID") then return false end
-- SuperWoW returns: current-form power, caster-form mana
local _, casterMana = UnitMana(unit)
-- Fallback: if for some reason we didn't get a 2nd value and we're in caster form now
if type(casterMana) ~= "number" then
if UnitPowerType and UnitPowerType(unit) == 0 then
casterMana = UnitMana(unit)
else
return false
end
end
local casterMana = CleveRoids.ClassicAPI.UnitPower(unit, 0)
local cmp = CleveRoids.comparators and CleveRoids.comparators[operator]
return cmp and cmp(casterMana, amount) or false
@@ -3363,10 +3350,7 @@ end
-- returns: True or false
function CleveRoids.ValidatePowerLost(unit, operator, amount)
if not unit or not operator or not amount then return false end
local API = CleveRoids.NampowerAPI
local maxPower = API and API.GetUnitMaxPower and API.GetUnitMaxPower(unit) or UnitManaMax(unit)
local power = API and API.GetUnitPower and API.GetUnitPower(unit) or UnitMana(unit)
local powerLost = maxPower - power
local powerLost = CleveRoids.ClassicAPI.UnitPowerMissing(unit)
if CleveRoids.operators[operator] then
return CleveRoids.comparators[operator](powerLost, amount)
@@ -3418,10 +3402,7 @@ end
-- returns: True or false
function CleveRoids.ValidateHpLost(unit, operator, amount)
if not unit or not operator or not amount then return false end
local API = CleveRoids.NampowerAPI
local maxHp = API and API.GetUnitMaxHealth and API.GetUnitMaxHealth(unit) or UnitHealthMax(unit)
local hp = API and API.GetUnitHealth and API.GetUnitHealth(unit) or UnitHealth(unit)
local hpLost = maxHp - hp
local hpLost = CleveRoids.ClassicAPI.UnitHealthMissing(unit)
if CleveRoids.operators[operator] then
return CleveRoids.comparators[operator](hpLost, amount)
@@ -3466,11 +3447,8 @@ function CleveRoids.ValidateCooldown(args, ignoreGCD)
-- If this is a numeric slot (1-19), resolve to the equipped item's name
local slotNum = tonumber(name)
if slotNum and slotNum >= 1 and slotNum <= 19 then
local link = GetInventoryItemLink("player", slotNum)
if link then
local _, _, itemName = string.find(link, "%[(.+)%]")
if itemName then name = itemName end
end
local itemName = C_Item.GetItemName({ equipmentSlotIndex = slotNum })
if itemName then name = itemName end
end
args = {name = name}
else
@@ -3481,11 +3459,8 @@ function CleveRoids.ValidateCooldown(args, ignoreGCD)
-- If this is a numeric slot (1-19), resolve to the equipped item's name
local slotNum = tonumber(name)
if slotNum and slotNum >= 1 and slotNum <= 19 then
local link = GetInventoryItemLink("player", slotNum)
if link then
local _, _, itemName = string.find(link, "%[(.+)%]")
if itemName then name = itemName end
end
local itemName = C_Item.GetItemName({ equipmentSlotIndex = slotNum })
if itemName then name = itemName end
end
args.name = name
else
@@ -3544,7 +3519,7 @@ local function GetLowercaseSpellName(spellID)
local cached = _spellNameCache[spellID]
if cached then return cached end
local name = GetSpellRecField(spellID, "name")
local name = C_Spell.GetSpellName(spellID)
if not name then return nil end
-- Strip rank and lowercase
@@ -3567,7 +3542,7 @@ local function GetSpellNames(spellID)
return cached.base, cached.full
end
local fullName = GetSpellRecField(spellID, "name")
local fullName = C_Spell.GetSpellName(spellID)
if not fullName then return nil, nil end
local baseName = _string_gsub(fullName, _RANK_PATTERN, "")
@@ -4222,7 +4197,7 @@ function CleveRoids.ValidateUnitDebuff(unit, args)
local fallbackNameLower = _string_lower(args.name)
for sid, rec in pairs(lib.objects[guid]) do
if rec and rec.caster == "player" then
local n = GetSpellRecField and GetSpellRecField(sid, "name")
local n = C_Spell.GetSpellName(sid)
if n then
n = CleveRoids.StripRank(n)
if _string_lower(n) == fallbackNameLower then
@@ -4309,7 +4284,7 @@ function CleveRoids.ValidateUnitDebuff(unit, args)
local cleanupNameLower = _string_lower(args.name)
for sid, rec in pairs(lib.objects[guid]) do
if rec and rec.caster == "player" then
local n = GetSpellRecField and GetSpellRecField(sid, "name")
local n = C_Spell.GetSpellName(sid)
if n then
n = CleveRoids.StripRank(n)
if _string_lower(n) == cleanupNameLower then
@@ -4643,19 +4618,9 @@ function CleveRoids.ValidatePlayerDebuff(args)
end
function CleveRoids.ValidateWeaponImbue(slot, args)
-- Check if weapon has enchant via API
local hasMainEnchant, mainExpiration, mainCharges, hasOffEnchant, offExpiration, offCharges = GetWeaponEnchantInfo()
local hasEnchant, expiration, charges
if slot == "mh" then
hasEnchant = hasMainEnchant
expiration = mainExpiration
charges = mainCharges
else
hasEnchant = hasOffEnchant
expiration = offExpiration
charges = offCharges
end
-- Temp-enchant state for this slot, read via ClassicAPI (centralizes the
-- 12-tuple indexing; enchantID unused here but powers [mhenchant]).
local hasEnchant, expiration, charges = CleveRoids.ClassicAPI.GetWeaponEnchant(slot)
-- Only consider temporary enchants (with time or charges)
-- This filters out permanent enchants like Crusader, Lifestealing, etc.
@@ -4770,6 +4735,19 @@ function CleveRoids.CheckWeaponImbueByName(slot, imbueName)
return true -- No name to check
end
-- Fast path: resolve the applied temp-enchant's ID -> localized name via
-- ClassicAPI (SpellItemEnchantment.dbc) and match that directly -- exact,
-- locale-clean, no green-text tooltip heuristics. Only nameless enchants
-- (e.g. sharpening stones, which show "+N Weapon Damage" and carry no DBC
-- name) return nil here and fall through to the tooltip scan below.
local _, _, _, enchantID = CleveRoids.ClassicAPI.GetWeaponEnchant(slot)
local enchantName = enchantID and CleveRoids.ClassicAPI.GetEnchantName(enchantID)
if enchantName then
local nlower = GetLowerNormalizedName(enchantName)
local want = GetLowerNormalizedName(imbueName)
return nlower == want or string.find(nlower, want, 1, true) ~= nil
end
-- Create tooltip scanner if needed
if not CleveRoidsTooltip then
CreateFrame("GameTooltip", "CleveRoidsTooltip", nil, "GameTooltipTemplate")
@@ -4810,6 +4788,48 @@ function CleveRoids.CheckWeaponImbueByName(slot, imbueName)
return false
end
-- Does a single [mhenchant]/[ohenchant] value match the applied enchant?
-- value is a numeric enchant ID (exact) or a name (resolved from the applied
-- enchantID via ClassicAPI's SpellItemEnchantment lookup -- no tooltip scan).
-- Name match is case-insensitive, exact-or-substring (so "Deadly" matches
-- "Deadly Poison"). A parsed-arg table (from an operator form) uses its .name.
local function EnchantValueMatches(value, enchantID)
if type(value) == "table" then value = value.name end
if not value or value == "" then return false end
local wantId = tonumber(value)
if wantId then
return enchantID == wantId
end
local name = CleveRoids.ClassicAPI.GetEnchantName(enchantID)
if not name then return false end
local nlower = GetLowerNormalizedName(name)
local want = GetLowerNormalizedName(value)
return nlower == want or string.find(nlower, want, 1, true) ~= nil
end
-- Matches the temporary weapon enchant on `slot` ("mh"/"oh") by enchant ID or
-- localized name, both resolved via ClassicAPI (C_Item.GetWeaponEnchantInfo +
-- GetEnchantInfo) -- locale-proof and exact, unlike the tooltip-scanned
-- [mhimbue:Name]. value may be true/nil (any temp enchant), a single ID/name,
-- or an OR-list array; returns true if the applied enchant matches any entry.
function CleveRoids.ValidateWeaponEnchant(slot, value)
local hasEnchant, expiration, charges, enchantID = CleveRoids.ClassicAPI.GetWeaponEnchant(slot)
local hasTemp = hasEnchant and ((expiration and expiration > 0) or (charges and charges > 0))
if not hasTemp then return false end
-- Bare [mhenchant]: any temporary enchant present.
if value == nil or value == true then return true end
-- OR-list (e.g. [mhenchant:2823/Deadly_Poison]) is an array of strings.
if type(value) == "table" and not value.name and table.getn(value) > 0 then
for i = 1, table.getn(value) do
if EnchantValueMatches(value[i], enchantID) then return true end
end
return false
end
return EnchantValueMatches(value, enchantID)
end
-- TODO: Look into https://github.com/Stanzilla/WoWUIBugs/issues/47 if needed
-- PERFORMANCE: Uncached version - called by GetCachedCooldown
function CleveRoids._GetCooldownUncached(name, ignoreGCD)
@@ -4869,13 +4889,11 @@ function CleveRoids.HasItem(item)
if type(item) == "string" and item ~= "" then
local itemLower = string.lower(item)
-- Check equipped slots for substring match
for slot = 0, 19 do
local link = GetInventoryItemLink("player", slot)
if link then
if string.find(string.lower(link), itemLower, 1, true) then
return true
end
-- Check equipped slots for substring match (decorated name, no link build)
for slot = 1, 19 do
local name = C_Item.GetItemName({ equipmentSlotIndex = slot })
if name and string.find(string.lower(name), itemLower, 1, true) then
return true
end
end
@@ -4884,11 +4902,9 @@ function CleveRoids.HasItem(item)
local size = GetContainerNumSlots(bag)
if size and size > 0 then
for slotIndex = 1, size do
local link = GetContainerItemLink(bag, slotIndex)
if link then
if string.find(string.lower(link), itemLower, 1, true) then
return true
end
local name = C_Item.GetItemName({ bagID = bag, slotIndex = slotIndex })
if name and string.find(string.lower(name), itemLower, 1, true) then
return true
end
end
end
@@ -4933,14 +4949,12 @@ function CleveRoids.GetItemCooldown(item)
local itemLower = string.lower(item)
local start, dur, en
-- Check equipped slots for substring match
for slot = 0, 19 do
local link = GetInventoryItemLink("player", slot)
if link then
if string.find(string.lower(link), itemLower, 1, true) then
start, dur, en = GetInventoryItemCooldown("player", slot)
return _norm(start, dur, en)
end
-- Check equipped slots for substring match (decorated name, no link build)
for slot = 1, 19 do
local name = C_Item.GetItemName({ equipmentSlotIndex = slot })
if name and string.find(string.lower(name), itemLower, 1, true) then
start, dur, en = GetInventoryItemCooldown("player", slot)
return _norm(start, dur, en)
end
end
@@ -4949,12 +4963,10 @@ function CleveRoids.GetItemCooldown(item)
local size = GetContainerNumSlots(bag)
if size and size > 0 then
for slotIndex = 1, size do
local link = GetContainerItemLink(bag, slotIndex)
if link then
if string.find(string.lower(link), itemLower, 1, true) then
start, dur, en = GetContainerItemCooldown(bag, slotIndex)
return _norm(start, dur, en)
end
local name = C_Item.GetItemName({ bagID = bag, slotIndex = slotIndex })
if name and string.find(string.lower(name), itemLower, 1, true) then
start, dur, en = GetContainerItemCooldown(bag, slotIndex)
return _norm(start, dur, en)
end
end
end
@@ -5111,7 +5123,7 @@ function CleveRoids.CheckSpellCast(unit, spell)
if not CleveRoids.spell_tracking[guid] then
return false
else
if spell == GetSpellRecField(CleveRoids.spell_tracking[guid].spell_id, "name") or (spell == "") then
if spell == C_Spell.GetSpellName(CleveRoids.spell_tracking[guid].spell_id) or (spell == "") then
return true
end
return false
@@ -5199,6 +5211,33 @@ function CleveRoids.GetSpellMechanic(spellID)
return CleveRoids.ClassicAPI.GetSpellMechanicByID(spellID) or 0
end
-- School name -> spell-school mask bit (WoW SPELL_SCHOOL_MASK_*).
local LOCKOUT_SCHOOL_MASK = {
physical = 1, holy = 2, fire = 4, nature = 8, frost = 16, shadow = 32, arcane = 64,
}
-- Is `bit` set in `mask`? Shift right past `bit`, then test the low bit with
-- math.mod (the % operator is 5.1-only; 1.12 is Lua 5.0, but math.mod exists).
local function SchoolMaskHasBit(mask, bit)
if not mask or mask < bit then return false end
return math.mod(math.floor(mask / bit), 2) == 1
end
-- [locked] / [locked:school] -- player is under a spell-school interrupt lockout
-- (Counterspell / Kick / Pummel / Earth Shock), read from C_LossOfControl. This
-- is the lockout no debuff scan can detect; full silences stay on [mycc:silence].
-- school nil/true = any school kicked; a name matches that school's mask bit.
function CleveRoids.ValidateSchoolLocked(school)
local mask = CleveRoids.ClassicAPI.GetSchoolLockout()
if not mask or mask == 0 then return false end
if school == nil or school == true or school == "" then
return true
end
local bit = LOCKOUT_SCHOOL_MASK[string.lower(school)]
if not bit then return false end
return SchoolMaskHasBit(mask, bit)
end
-- Validate CC on a unit (target, focus, player, etc.)
-- Returns true if the unit has the specified CC mechanic active
function CleveRoids.ValidateUnitCC(unit, ccType)
@@ -5447,6 +5486,33 @@ local function ResolveFocusUnit(unit)
return unit
end
-- Builds a Keyword validator for a specific power slot (Enum.PowerType) read as a
-- percentage, mirroring [power]/[mypower]. condKey is the conditional name;
-- unitDefault is "player" (self) or "target" (@unit-overridable); powerType is the
-- Enum.PowerType id. Handles the multi-comparison (>50&<80) branch like [power].
local function MakeTypedPowerKeyword(condKey, unitDefault, powerType)
return function(conditionals)
return Multi(conditionals[condKey], function(args)
if type(args) ~= "table" then return false end
local unit = (unitDefault == "player") and "player" or (conditionals.target or "target")
if args.comparisons and type(args.comparisons) == "table" then
if not UnitExists(unit) then return false end
local maxPower = CleveRoids.ClassicAPI.UnitPowerMax(unit, powerType)
if not maxPower or maxPower <= 0 then return false end
local powerPercent = 100 * CleveRoids.ClassicAPI.UnitPower(unit, powerType) / maxPower
for _, comp in ipairs(args.comparisons) do
if not CleveRoids.operators[comp.operator] then return false end
if not CleveRoids.comparators[comp.operator](powerPercent, comp.amount) then return false end
end
return true
end
return CleveRoids.ValidateTypedPower(unit, powerType, args.operator, args.amount)
end, conditionals, condKey)
end
end
-- A list of Conditionals and their functions to validate them
CleveRoids.Keywords = {
exists = function(conditionals)
@@ -5630,6 +5696,23 @@ CleveRoids.Keywords = {
return not CleveRoids.ClassicAPI.IsStealthed()
end,
mounted = function(conditionals)
return CleveRoids.ClassicAPI.IsMounted()
end,
nomounted = function(conditionals)
return not CleveRoids.ClassicAPI.IsMounted()
end,
standing = function(conditionals)
return CleveRoids.ClassicAPI.GetPlayerStandState() == 0
end,
-- Any non-standing pose (sit / chair / sleep / kneel)
sitting = function(conditionals)
return CleveRoids.ClassicAPI.GetPlayerStandState() ~= 0
end,
casting = function(conditionals)
if type(conditionals.casting) ~= "table" then return CleveRoids.CheckSpellCast(conditionals.target, "") end
return Or(conditionals.casting, function (spell)
@@ -5808,6 +5891,26 @@ CleveRoids.Keywords = {
end, conditionals, "noset")
end,
-- [equipset:Name] — true if the saved equipment set "Name" is currently
-- equipped (ClassicAPI equipment manager, distinct from [set] tier pieces).
-- [equipset:Raid/PvP] = either set equipped. Names are case-sensitive; use _
-- for spaces, e.g. [equipset:My_Raid_Set].
equipset = function(conditionals)
return Multi(conditionals.equipset, function(v)
local name = (type(v) == "table") and v.name or v
return CleveRoids.ClassicAPI.IsEquipmentSetEquipped(name)
end, conditionals, "equipset")
end,
-- [noequipset:Name] — true if that equipment set is NOT currently equipped.
-- [noequipset:Raid/PvP] = neither equipped (De Morgan's).
noequipset = function(conditionals)
return NegatedMulti(conditionals.noequipset, function(v)
local name = (type(v) == "table") and v.name or v
return not CleveRoids.ClassicAPI.IsEquipmentSetEquipped(name)
end, conditionals, "noequipset")
end,
-- [inbag:Item] — true if item exists in bags or equipped
-- [inbag:Item<12] — true if bag count of Item is less than 12
-- Supports multi-value: [inbag:Item1/Item2 inbag:Item3] = (Item1 OR Item2) AND Item3
@@ -5887,13 +5990,10 @@ CleveRoids.Keywords = {
local itemName = name
local slotNum = tonumber(name)
if slotNum and slotNum >= 1 and slotNum <= 19 then
-- Resolve slot number to item name
local link = GetInventoryItemLink("player", slotNum)
if link then
local _, _, extractedName = string.find(link, "%[(.+)%]")
if extractedName then
itemName = extractedName
end
-- Resolve slot number to item name (decorated, no link build)
local extractedName = C_Item.GetItemName({ equipmentSlotIndex = slotNum })
if extractedName then
itemName = extractedName
end
end
@@ -5925,13 +6025,10 @@ CleveRoids.Keywords = {
local itemName = name
local slotNum = tonumber(name)
if slotNum and slotNum >= 1 and slotNum <= 19 then
-- Resolve slot number to item name
local link = GetInventoryItemLink("player", slotNum)
if link then
local _, _, extractedName = string.find(link, "%[(.+)%]")
if extractedName then
itemName = extractedName
end
-- Resolve slot number to item name (decorated, no link build)
local extractedName = C_Item.GetItemName({ equipmentSlotIndex = slotNum })
if extractedName then
itemName = extractedName
end
end
@@ -6036,14 +6133,14 @@ CleveRoids.Keywords = {
local groupVal = conditionals.group
-- Boolean form [group] - check if in any group
if groupVal == true then
return GetNumPartyMembers() > 0 or GetNumRaidMembers() > 0
return IsInGroup()
end
-- Value form [group:party] or [group:raid] or [group:party/raid]
return Multi(groupVal, function(groupType)
if groupType == "party" then
return GetNumPartyMembers() > 0
return IsInGroup()
elseif groupType == "raid" then
return GetNumRaidMembers() > 0
return IsInRaid()
end
return false
end, conditionals, "group")
@@ -6055,14 +6152,14 @@ CleveRoids.Keywords = {
local groupVal = conditionals.nogroup
-- Boolean form [nogroup] - check if not in any group
if groupVal == true then
return GetNumPartyMembers() == 0 and GetNumRaidMembers() == 0
return not IsInGroup()
end
-- Value form with De Morgan's law via NegatedMulti
return NegatedMulti(groupVal, function(groupType)
if groupType == "party" then
return GetNumPartyMembers() == 0
return not IsInGroup()
elseif groupType == "raid" then
return GetNumRaidMembers() == 0
return not IsInRaid()
end
return true
end, conditionals, "nogroup")
@@ -6282,6 +6379,17 @@ CleveRoids.Keywords = {
end, conditionals, "mypower")
end,
-- Type-specific power reads (percentage), for the SPECIFIC slot rather than the
-- unit's primary power. No prefix = target (@unit-overridable), my = player.
-- e.g. [@target,mana:<15] catches a near-OOM caster; [myenergy:>50] gates a
-- druid's cat rotation regardless of form.
mana = MakeTypedPowerKeyword("mana", "target", 0),
mymana = MakeTypedPowerKeyword("mymana", "player", 0),
rage = MakeTypedPowerKeyword("rage", "target", 1),
myrage = MakeTypedPowerKeyword("myrage", "player", 1),
energy = MakeTypedPowerKeyword("energy", "target", 3),
myenergy = MakeTypedPowerKeyword("myenergy", "player", 3),
rawpower = function(conditionals)
return Multi(conditionals.rawpower, function(args)
if type(args) ~= "table" then return false end
@@ -7244,6 +7352,26 @@ CleveRoids.Keywords = {
return not CleveRoids.ClassicAPI.IsSwimming()
end,
-- [indoors] - Player is under a WMO roof (building, cave, instance interior)
indoors = function(conditionals)
return CleveRoids.ClassicAPI.IsIndoors()
end,
-- [noindoors] - Player is NOT indoors
noindoors = function(conditionals)
return not CleveRoids.ClassicAPI.IsIndoors()
end,
-- [outdoors] - Player is outdoors (open sky / not inside a WMO interior)
outdoors = function(conditionals)
return CleveRoids.ClassicAPI.IsOutdoors()
end,
-- [nooutdoors] - Player is NOT outdoors
nooutdoors = function(conditionals)
return not CleveRoids.ClassicAPI.IsOutdoors()
end,
-- [rooted] - Player is currently rooted (Nampower v2.36+)
rooted = function(conditionals)
if not CleveRoids.NampowerAPI.features.hasPlayerIsRooted then
@@ -7616,6 +7744,24 @@ CleveRoids.Keywords = {
return not CleveRoids.ValidateWeaponImbue("oh", args)
end,
-- [mhenchant:ID] / [mhenchant:Name] - main hand has that SPECIFIC temporary
-- weapon enchant, matched by SpellItemEnchantment ID or localized name via
-- ClassicAPI (locale-proof, unlike [mhimbue:Name]'s tooltip scan). Bare
-- [mhenchant] = any temp enchant; [mhenchant:A/B] = either; [nomhenchant:X]
-- = not that enchant. Names use _ for spaces, e.g. [mhenchant:Deadly_Poison].
mhenchant = function(conditionals)
return CleveRoids.ValidateWeaponEnchant("mh", conditionals.mhenchant)
end,
nomhenchant = function(conditionals)
return not CleveRoids.ValidateWeaponEnchant("mh", conditionals.nomhenchant)
end,
ohenchant = function(conditionals)
return CleveRoids.ValidateWeaponEnchant("oh", conditionals.ohenchant)
end,
noohenchant = function(conditionals)
return not CleveRoids.ValidateWeaponEnchant("oh", conditionals.noohenchant)
end,
immune = function(conditionals)
-- Check if target is immune to the spell being cast or damage school
-- Usage: [immune] SpellName OR [immune:SpellName] OR [immune:fire]
@@ -8275,7 +8421,7 @@ CleveRoids.Keywords = {
if not UnitExists(unit) then return false end
return Or(conditionals.powertype, function(powerTypeName)
local powerType = UnitPowerType(unit)
local powerType = CleveRoids.ClassicAPI.UnitPowerType(unit)
local powerTypeLower = string.lower(powerTypeName or "")
if powerTypeLower == "mana" then
@@ -8298,7 +8444,7 @@ CleveRoids.Keywords = {
if not UnitExists(unit) then return true end
return NegatedMulti(conditionals.nopowertype, function(powerTypeName)
local powerType = UnitPowerType(unit)
local powerType = CleveRoids.ClassicAPI.UnitPowerType(unit)
local powerTypeLower = string.lower(powerTypeName or "")
if powerTypeLower == "mana" then
@@ -8383,6 +8529,33 @@ CleveRoids.Keywords = {
end, conditionals, "nomycc")
end,
-- [locked] / [locked:school] - player is under a spell-school interrupt
-- lockout (Counterspell/Kick/Pummel/Earth Shock), via ClassicAPI
-- C_LossOfControl -- the server lockout no debuff scan can see. Bare = any
-- school kicked; [locked:frost] = that school; [locked:fire/frost] = either.
-- Full silences are separate ([mycc:silence]). Player-only.
locked = function(conditionals)
local v = conditionals.locked
if v == nil or v == true or (type(v) == "table" and table.getn(v) == 0) then
return CleveRoids.ValidateSchoolLocked(nil)
end
return Or(v, function(school)
return CleveRoids.ValidateSchoolLocked(school)
end)
end,
-- [nolocked:school] - player is NOT school-locked. AND logic on negation:
-- [nolocked:fire/frost] = neither Fire nor Frost is kicked.
nolocked = function(conditionals)
local v = conditionals.nolocked
if v == nil or v == true or (type(v) == "table" and table.getn(v) == 0) then
return not CleveRoids.ValidateSchoolLocked(nil)
end
return NegatedMulti(v, function(school)
return not CleveRoids.ValidateSchoolLocked(school)
end, conditionals, "nolocked")
end,
-- ========================================================================
-- RESIST TRACKING CONDITIONALS
-- ========================================================================
@@ -9210,6 +9383,7 @@ CleveRoids.STATIC_CONDITIONALS = {
combat = true, nocombat = true, ic = true, ooc = true,
zone = true, nozone = true,
stealth = true, nostealth = true, stl = true, nostl = true,
mounted = true, nomounted = true, standing = true, sitting = true,
form = true, noform = true, stance = true, nostance = true,
equipped = true, noequipped = true, eq = true, noeq = true,
set = true, noset = true,
@@ -9217,6 +9391,7 @@ CleveRoids.STATIC_CONDITIONALS = {
mod = true, nomod = true,
keydown = true, nokeydown = true,
swimming = true, noswimming = true, swim = true, noswim = true,
indoors = true, noindoors = true, outdoors = true, nooutdoors = true,
rooted = true, norooted = true,
resting = true, noresting = true,
}
+14
View File
@@ -63,10 +63,24 @@ SlashCmdList.EQSLOT13 = CleveRoids.DoEquipTrinket1
SLASH_EQSLOT141 = "/equip14"
SlashCmdList.EQSLOT14 = CleveRoids.DoEquipTrinket2
-- Reclaim ClassicAPI's /equipset (command name EQUIP_SET) so it supports
-- conditionals. ClassicAPI registers the localized aliases against EQUIP_SET;
-- swapping the handler keeps every alias and adds the conditional engine.
if SlashCmdList.EQUIP_SET then
SlashCmdList.EQUIP_SET = CleveRoids.DoEquipSet
else
SLASH_EQUIPSET1 = "/equipset"
SlashCmdList.EQUIPSET = CleveRoids.DoEquipSet
end
SLASH_UNSHIFT1 = "/unshift"
SlashCmdList.UNSHIFT = CleveRoids.DoUnshift
SLASH_CANCELFORM1 = "/cancelform"
SlashCmdList.CANCELFORM = CleveRoids.DoCancelForm
SLASH_UNQUEUE1 = "/unqueue"
SlashCmdList.UNQUEUE = SpellStopCasting
+332 -545
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -209,8 +209,8 @@ local function InjectCustomSpells()
local count = 0
for spellID, data in pairs(CleveRoids.CustomCursiveSpells) do
-- Get texture from GetSpellRecField + GetSpellIconTexture
local name = GetSpellRecField(spellID, "name")
local rank = GetSpellRecField(spellID, "rank")
local name = C_Spell.GetSpellName(spellID)
local rank = C_Spell.GetSpellSubtext(spellID)
local texture = CleveRoids.libdebuff and CleveRoids.libdebuff:GetCachedIcon(spellID)
if texture then
-- Always update/add (in case Cursive reloaded and cleared them)
@@ -524,8 +524,8 @@ CleveRoids.HandleConsoleCommand = function(msg)
return
end
local name = GetSpellRecField(spellID, "name")
local rank = GetSpellRecField(spellID, "rank")
local name = C_Spell.GetSpellName(spellID)
local rank = C_Spell.GetSpellSubtext(spellID)
local texture = CleveRoids.libdebuff and CleveRoids.libdebuff:GetCachedIcon(spellID)
if not name then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Error:|r Spell ID " .. spellID .. " not found.")
+3 -13
View File
@@ -1391,12 +1391,6 @@ end
-- Extension Entry Points
-- ============================================================================
function Extension.OnAddonLoaded()
if arg1 == "Blizzard_MacroUI" then
InstallHooks()
end
end
function Extension.OnLoad()
-- Skip if macro checker is disabled
if CleveRoidMacros and CleveRoidMacros.macrocheck == 0 then return end
@@ -1404,13 +1398,9 @@ function Extension.OnLoad()
-- Skip if SuperMacro is loaded (detected at load time)
if SuperMacroFrame ~= nil then return end
-- Listen for macro UI loading
Extension.RegisterEvent("ADDON_LOADED", "OnAddonLoaded")
-- If MacroFrame already exists (unlikely but safe), hook immediately
if MacroFrame and MacroFrameText then
InstallHooks()
end
-- Install once Blizzard's macro UI is available (fires immediately if already
-- loaded), replacing the ADDON_LOADED listener + manual "already loaded" check.
EventUtil.ContinueOnAddOnLoaded("Blizzard_MacroUI", InstallHooks)
end
_G["CleveRoids"] = CleveRoids
+4 -15
View File
@@ -133,12 +133,6 @@ function Extension.OnMacroFrameLoad()
end
end
function Extension.OnAddonLoaded()
if arg1 == "Blizzard_MacroUI" then
Extension.OnMacroFrameLoad()
end
end
function Extension.OnLoad()
-- Schedule messages to show after UI is ready
local function ShowMessages()
@@ -178,16 +172,11 @@ function Extension.OnLoad()
end
end
-- Listen for macro UI loading
Extension.RegisterEvent("ADDON_LOADED", "OnAddonLoaded")
-- Hook the macro UI once available (fires immediately if already loaded).
EventUtil.ContinueOnAddOnLoaded("Blizzard_MacroUI", Extension.OnMacroFrameLoad)
-- Also try to hook MacroFrame_SaveMacro if it already exists
if MacroFrame_SaveMacro then
Extension.OnMacroFrameLoad()
end
-- Register PLAYER_LOGIN to show status messages
Extension.RegisterEvent("PLAYER_LOGIN", "OnPlayerLogin")
-- Status messages on login (currently disabled inside OnPlayerLogin).
EventUtil.ContinueOnPlayerLogin(Extension.OnPlayerLogin)
-- Store the message function for later
Extension.ShowMessages = ShowMessages
+4 -6
View File
@@ -6,7 +6,6 @@ local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
local Extension = CleveRoids.RegisterExtension("CT_RaidAssist")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnter()
local tempOptions = CT_RAMenu_Options["temp"]
@@ -48,15 +47,14 @@ function Extension.OnLeave()
CleveRoids.ClearMouseoverFrom("native")
end
function Extension.OnLoad()
if arg1 ~= "CT_RaidAssist" then
return
end
function Extension.OnLoad() end
function Extension.OnAddOnLoad()
if not CT_RA_MemberFrame_OnEnter then return end
Extension.Hook("CT_RA_MemberFrame_OnEnter", "OnEnter")
Extension.HookMethod(_G["GameTooltip"], "Hide", "OnLeave")
Extension.HookMethod(_G["GameTooltip"], "FadeOut", "OnLeave")
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("CT_RaidAssist", Extension.OnAddOnLoad)
+5 -4
View File
@@ -6,7 +6,6 @@ local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
local Extension = CleveRoids.RegisterExtension("CT_UnitFrames")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.SetHook(widget)
local hookedOnEnter = widget:GetScript("OnEnter")
@@ -24,8 +23,10 @@ function Extension.SetHook(widget)
end)
end
function Extension.OnLoad()
if arg1 ~= "CT_UnitFrames" or not CT_AssistFrame then
function Extension.OnLoad() end
function Extension.OnAddOnLoad()
if not CT_AssistFrame then
return
end
CleveRoids.Print("CT_UnitFrames module loaded.")
@@ -36,4 +37,4 @@ function Extension.OnLoad()
Extension.SetHook(CT_AssistFrame_Drag)
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("CT_UnitFrames", Extension.OnAddOnLoad)
+8 -17
View File
@@ -9,7 +9,6 @@ local CleveRoids = _G.CleveRoids or {}
CleveRoids.Hooks = CleveRoids.Hooks or {}
local Extension = CleveRoids.RegisterExtension("CursiveMouseover")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
local hooked = false
@@ -66,21 +65,13 @@ local function HookCursiveUI()
end
function Extension.OnLoad()
-- Try to hook when Cursive loads
if arg1 == "Cursive" then
-- Delay slightly to ensure Cursive.ui is initialized
local frame = CreateFrame("Frame")
frame:SetScript("OnUpdate", function()
if HookCursiveUI() then
this:Hide()
end
end)
end
-- Delay slightly to ensure Cursive.ui is initialized
local frame = CreateFrame("Frame")
frame:SetScript("OnUpdate", function()
if HookCursiveUI() then
this:Hide()
end
end)
end
-- Also try to hook immediately in case Cursive is already loaded
if Cursive and Cursive.ui then
HookCursiveUI()
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("Cursive", Extension.OnLoad)
+4 -6
View File
@@ -6,7 +6,6 @@ local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
local Extension = CleveRoids.RegisterExtension("DiscordUnitFrames")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnterFrame()
CleveRoids.SetMouseoverFrom("duf", this.unit)
@@ -26,11 +25,10 @@ function Extension.OnLeaveElement()
CleveRoids.ClearMouseoverFrom("native")
end
function Extension.OnLoad()
if arg1 ~= "DiscordUnitFrames" then
return
end
function Extension.OnLoad() end
function Extension.OnAddOnLoad()
if not DUF_UnitFrame_OnEnter then return end
CleveRoids.ClearHooks()
Extension.Hook("DUF_UnitFrame_OnEnter", "OnEnterFrame")
Extension.Hook("DUF_UnitFrame_OnLeave", "OnLeaveFrame")
@@ -39,4 +37,4 @@ function Extension.OnLoad()
Extension.Hook("DUF_Element_OnLeave", "OnLeaveElement")
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("DiscordUnitFrames", Extension.OnAddOnLoad)
+4 -6
View File
@@ -8,7 +8,6 @@ local CleveRoids = _G.CleveRoids or {}
CleveRoids.Hooks = CleveRoids.Hooks or {}
local Extension = CleveRoids.RegisterExtension("Grid")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnter(unit)
CleveRoids.SetMouseoverFrom("grid", unit)
@@ -19,11 +18,10 @@ function Extension.OnLeave()
CleveRoids.ClearMouseoverFrom("native")
end
function Extension.OnLoad()
if arg1 ~= "Grid" then
return
end
function Extension.OnLoad() end
function Extension.OnAddOnLoad()
if not GridFrame then return end
CleveRoids.Hooks.Grid = { CreateFrames = GridFrame.frameClass.prototype.CreateFrames}
GridFrame.frameClass.prototype.CreateFrames = CleveRoids.GrdCreateFrames
end
@@ -111,4 +109,4 @@ function CleveRoids:GrdCreateFrames()
ClickCastFrames[self.frame] = true
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("Grid", Extension.OnAddOnLoad)
+4 -4
View File
@@ -8,7 +8,6 @@ local CleveRoids = _G.CleveRoids or {}
local CreateFrames = nil
local Extension = CleveRoids.RegisterExtension("NotGrid")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnter()
CleveRoids.SetMouseoverFrom("ngrid", this.unit)
@@ -39,7 +38,9 @@ function CleveRoids:NotGrid_CreateFrames()
end
end
function Extension.OnLoad()
function Extension.OnLoad() end
function Extension.OnAddOnLoad()
-- NotGrid loads before CleveRoids, so if NotGrid is enabled, then it's global will exist.
if not NotGrid then
return
@@ -48,7 +49,6 @@ function Extension.OnLoad()
CreateFrames = NotGrid.CreateFrames
NotGrid.CreateFrames = CleveRoids.NotGrid_CreateFrames
Extension.UnregisterEvent("ADDON_LOADED", "Onload")
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("NotGrid", Extension.OnAddOnLoad)
+4 -6
View File
@@ -8,7 +8,6 @@ local CleveRoids = _G.CleveRoids or {}
CleveRoids.Hooks = CleveRoids.Hooks or {}
local Extension = CleveRoids.RegisterExtension("PerfectRaid")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnter(unit)
CleveRoids.SetMouseoverFrom("praid", unit)
@@ -19,11 +18,10 @@ function Extension.OnLeave()
CleveRoids.ClearMouseoverFrom("native")
end
function Extension.OnLoad()
if arg1 ~= "PerfectRaid" then
return
end
function Extension.OnLoad() end
function Extension.OnAddOnLoad()
if not PerfectRaid then return end
CleveRoids.Hooks.PerfectRaid = { CreateFrame = PerfectRaid.CreateFrame }
PerfectRaid.CreateFrame = CleveRoids.PerfectRaidCreateFrame
end
@@ -134,4 +132,4 @@ function CleveRoids.PerfectRaidCreateFrame(self, num)
--]]
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("PerfectRaid", Extension.OnAddOnLoad)
+1 -3
View File
@@ -4,7 +4,6 @@ local CleveRoids = _G.CleveRoids or {}
CleveRoids.Hooks = CleveRoids.Hooks or {}
local Extension = CleveRoids.RegisterExtension("ag_UnitFrames")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnter(unit)
CleveRoids.SetMouseoverFrom("aguf", unit)
@@ -23,7 +22,6 @@ function Extension.OnLoad()
CleveRoids.Hooks.ag_UnitFrames = { OnEnter = aUF.classes.aUFunit.prototype.OnEnter, OnLeave = aUF.classes.aUFunit.prototype.OnLeave}
aUF.classes.aUFunit.prototype.OnEnter = CleveRoids.aUFOnEnter
aUF.classes.aUFunit.prototype.OnLeave = CleveRoids.aUFOnLeave
Extension.UnregisterEvent("ADDON_LOADED", "Onload")
end
-- Taken from ag_UnitClass.lua
@@ -41,4 +39,4 @@ function CleveRoids:aUFOnLeave()
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("ag_UnitFrames", Extension.OnLoad)
+14 -248
View File
@@ -2,11 +2,17 @@
Author: Dennis Werner Garske (DWG) / brian / Mewtiny
License: MIT License
Fixes pfUI mouseover issues by:
- Using a unique source key per pfUI frame (e.g., "pfui:party3", "pfui:raid7")
- Pairing Set/Clear with the same per-frame key
- 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"
pfUI integration. pfUI's own unitframes now set the native mouseover unit
(Nampower SetMouseoverUnit, via pfUI.uf.OnEnter bound in pfUI.uf:EnableScripts
on every unitframe), so [@mouseover]/[mouseover] resolve against pfUI frames
through the native "mouseover" token -- every consumer checks UnitExists(
"mouseover") before the CleveRoids.mouseoverUnit fallback, so no per-frame
hooking is needed here anymore. What remains is the two things pfUI doesn't
cover:
- Raid-marker rows: NOT unitframes (never go through EnableScripts), so pfUI
sets no mouseover for them. Hooked so hovering registers "mark1".."mark8"
via CleveRoids.mouseoverUnit.
- /pfcast: wrapped so its argument runs through CleveRoids conditionals.
]]
local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
@@ -95,241 +101,11 @@ local function PfClear(frame)
end
end
-- PLAYER
function Extension.RegisterPlayerScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.player then return end
local frame = pfUI.uf.player
local onEnterFunc = frame:GetScript("OnEnter")
local onLeaveFunc = frame:GetScript("OnLeave")
frame:SetScript("OnEnter", function()
PfSet(this, "player")
if onEnterFunc then onEnterFunc(this) end
end)
frame:SetScript("OnLeave", function()
PfClear(this)
if onLeaveFunc then onLeaveFunc(this) end
end)
end
-- TARGET
function Extension.RegisterTargetScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.target then return end
local frame = pfUI.uf.target
local onEnterFunc = frame:GetScript("OnEnter")
local onLeaveFunc = frame:GetScript("OnLeave")
frame:SetScript("OnEnter", function()
PfSet(this, "target")
if onEnterFunc then onEnterFunc(this) end
end)
frame:SetScript("OnLeave", function()
PfClear(this)
if onLeaveFunc then onLeaveFunc(this) end
end)
end
-- TARGETTARGET
function Extension.RegisterTargetTargetScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.targettarget then return end
local frame = pfUI.uf.targettarget
local onEnterFunc = frame:GetScript("OnEnter")
local onLeaveFunc = frame:GetScript("OnLeave")
frame:SetScript("OnEnter", function()
PfSet(this, "targettarget")
if onEnterFunc then onEnterFunc(this) end
end)
frame:SetScript("OnLeave", function()
PfClear(this)
if onLeaveFunc then onLeaveFunc(this) end
end)
end
-- PARTY (pfUI.uf.group[0..4]) -- include 0 to cover your own party slot
function Extension.RegisterPartyScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.group then return end
local i
for i = 0, 4 do
local frame = pfUI.uf.group[i]
if frame then
-- bind loop index for closures (Vanilla-safe)
local idx = i
local onEnterFunc = frame:GetScript("OnEnter")
local onLeaveFunc = frame:GetScript("OnLeave")
frame:SetScript("OnEnter", function()
-- For group[0] (your own party frame), default to "player"
local defaultUnit = (idx == 0) and "player" or nil
PfSet(this, defaultUnit)
if onEnterFunc then onEnterFunc(this) end
end)
frame:SetScript("OnLeave", function()
PfClear(this)
if onLeaveFunc then onLeaveFunc(this) end
end)
end
end
end
-- RAID (pfUI.uf.raid[1..40])
function Extension.RegisterRaidScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.raid then return end
local i
for i = 1, 40 do
local frame = pfUI.uf.raid[i]
if frame then
local onEnterFunc = frame:GetScript("OnEnter")
local onLeaveFunc = frame:GetScript("OnLeave")
frame:SetScript("OnEnter", function()
PfSet(this)
if onEnterFunc then onEnterFunc(this) end
end)
frame:SetScript("OnLeave", function()
PfClear(this)
if onLeaveFunc then onLeaveFunc(this) end
end)
end
end
end
-- FOCUS
function Extension.RegisterFocusScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.focus then return end
local frame = pfUI.uf.focus
local onEnterFunc = frame:GetScript("OnEnter")
local onLeaveFunc = frame:GetScript("OnLeave")
frame:SetScript("OnEnter", function()
PfSet(this) -- ResolvePfUnit handles focus emulation
if onEnterFunc then onEnterFunc(this) end
end)
frame:SetScript("OnLeave", function()
PfClear(this)
if onLeaveFunc then onLeaveFunc(this) end
end)
end
-- FOCUSTARGET (if your pfUI build provides it)
function Extension.RegisterFocusTargetScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.focustarget then return end
local frame = pfUI.uf.focustarget
local onEnterFunc = frame:GetScript("OnEnter")
local onLeaveFunc = frame:GetScript("OnLeave")
frame:SetScript("OnEnter", function()
PfSet(this)
if onEnterFunc then onEnterFunc(this) end
end)
frame:SetScript("OnLeave", function()
PfClear(this)
if onLeaveFunc then onLeaveFunc(this) end
end)
end
-- PETTARGET (if your pfUI build provides it)
function Extension.RegisterPetTargetScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.pettarget then return end
local frame = pfUI.uf.pettarget
local onEnterFunc = frame:GetScript("OnEnter")
local onLeaveFunc = frame:GetScript("OnLeave")
frame:SetScript("OnEnter", function()
PfSet(this)
if onEnterFunc then onEnterFunc(this) end
end)
frame:SetScript("OnLeave", function()
PfClear(this)
if onLeaveFunc then onLeaveFunc(this) end
end)
end
-- TARGETTARGETTARGET (if your pfUI build provides it)
function Extension.RegisterTargetTargetTargetScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.targettargettarget then return end
local frame = pfUI.uf.targettargettarget
local onEnterFunc = frame:GetScript("OnEnter")
local onLeaveFunc = frame:GetScript("OnLeave")
frame:SetScript("OnEnter", function()
PfSet(this)
if onEnterFunc then onEnterFunc(this) end
end)
frame:SetScript("OnLeave", function()
PfClear(this)
if onLeaveFunc then onLeaveFunc(this) end
end)
end
-- PARTYTARGET (party1target..party4target, plus player's target)
function Extension.RegisterPartyTargetScripts()
if not pfUI or not pfUI.uf then return end
-- This helper function is used to hook any given frame.
local function hookFrame(frame, defaultUnit)
if not frame then return end
local onEnterFunc = frame:GetScript("OnEnter")
local onLeaveFunc = frame:GetScript("OnLeave")
frame:SetScript("OnEnter", function()
PfSet(this, defaultUnit)
-- We remove the call to the original onEnterFunc to prevent overwritten tooltips.
end)
frame:SetScript("OnLeave", function()
PfClear(this)
if onLeaveFunc then onLeaveFunc(this) end
end)
end
-- This helper function is specifically for party member targets (1-4)
local function hookPartyMemberTarget(i, frame)
if not frame then return end
local defaultUnit = "party" .. i .. "target"
hookFrame(frame, defaultUnit)
end
-- Case A: Hook dedicated arrays for party members 1-4
if pfUI.uf.grouptarget then
for i = 1, 4 do hookPartyMemberTarget(i, pfUI.uf.grouptarget[i]) end
end
if pfUI.uf.partytarget then
for i = 1, 4 do hookPartyMemberTarget(i, pfUI.uf.partytarget[i]) end
end
-- Case B: Hook child target frames for party members 1-4
if pfUI.uf.group then
for i = 1, 4 do
local g = pfUI.uf.group[i]
if g and g.target then hookPartyMemberTarget(i, g.target) end
end
end
--- START OF FIX to include party0target ---
-- Case C: Specifically find and hook the player's own target frame (group[0].target)
if pfUI.uf.group and pfUI.uf.group[0] and pfUI.uf.group[0].target then
-- The player's target UnitID is always "target", not "party0target".
hookFrame(pfUI.uf.group[0].target, "target")
end
--- END OF FIX ---
end
-- RAID MARKERS (pfUI raidmarkers module)
-- Rows are plain Buttons with label="mark" and id=1-8. They have no OnEnter/OnLeave
-- by default, so [mouseover] macros are blind to them. We hook each row so hovering
-- registers "mark1".."mark8" through the normal priority system.
-- registers "mark1".."mark8" through the normal priority system. pfUI's own
-- SetMouseoverUnit path only covers unitframes, so this stays.
function Extension.RegisterRaidMarkScripts()
if not pfUI or not pfUI.raidmarkers or not pfUI.raidmarkers.rows then return end
@@ -371,17 +147,7 @@ function Extension.HookPfCast()
end
function Extension.PLAYER_ENTERING_WORLD()
if not pfUI or not pfUI.uf then return end
Extension.RegisterPlayerScripts()
Extension.RegisterTargetScripts()
Extension.RegisterTargetTargetScripts()
Extension.RegisterPartyScripts()
Extension.RegisterPartyTargetScripts()
Extension.RegisterRaidScripts()
Extension.RegisterFocusScripts()
Extension.RegisterFocusTargetScripts()
Extension.RegisterPetTargetScripts()
Extension.RegisterTargetTargetTargetScripts()
if not pfUI then return end
Extension.RegisterRaidMarkScripts()
Extension.HookPfCast()
end
+4 -6
View File
@@ -6,7 +6,6 @@ local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
local Extension = CleveRoids.RegisterExtension("sRaidFrames")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension:OnEnter(frame)
CleveRoids.SetMouseoverFrom("sraid", frame.unit)
@@ -17,14 +16,13 @@ function Extension.OnLeave()
CleveRoids.ClearMouseoverFrom("native")
end
function Extension.OnLoad()
if arg1 ~= "sRaidFrames" then
return
end
function Extension.OnLoad() end
function Extension.OnAddOnLoad()
if not sRaidFrames then return end
Extension.HookMethod(sRaidFrames, "UnitTooltip", "OnEnter")
Extension.HookMethod(_G["GameTooltip"], "Hide", "OnLeave")
Extension.HookMethod(_G["GameTooltip"], "FadeOut", "OnLeave")
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("sRaidFrames", Extension.OnAddOnLoad)
+2 -2
View File
@@ -122,7 +122,7 @@ local function CreateIconButton(parent, index, iconTable)
GameTooltip:SetOwner(btn, "ANCHOR_BOTTOMLEFT")
local spellName
if data.spellId then
spellName = GetSpellRecField and GetSpellRecField(data.spellId, "name") or ("Spell " .. data.spellId)
spellName = C_Spell.GetSpellName(data.spellId) or ("Spell " .. data.spellId)
else
spellName = data.displayName
end
@@ -649,7 +649,7 @@ local function InjectTestTargetData()
for i = 1, table.getn(TEST_TARGET_SPELL_IDS) do
local spellId = TEST_TARGET_SPELL_IDS[i]
local dur = targetDurations[i] or 60
local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") or ("TestSpell" .. spellId)
local spellName = C_Spell.GetSpellName(spellId) or ("TestSpell" .. spellId)
if not CleveRoids.AllCasterAuraTracking[targetGuid][spellName] then
CleveRoids.AllCasterAuraTracking[targetGuid][spellName] = {}
end
+71 -100
View File
@@ -58,11 +58,7 @@ function CleveRoids.IndexSpells()
bookType = CleveRoids.bookTypes[book]
spells[bookType] = {}
else
local cost, reagent = CleveRoids.GetSpellCost(i, bookType)
-- Fallback for known reagent spells if tooltip scan failed
if (not reagent or reagent == "") and CleveRoids.ReagentBySpell then
reagent = CleveRoids.ReagentBySpell[spellName]
end
local cost, reagent, reagentId = CleveRoids.GetSpellCost(i, bookType, spellId)
if not spells[bookType][spellName] then
spells[bookType][spellName] = {
spellSlot = i,
@@ -71,7 +67,7 @@ function CleveRoids.IndexSpells()
bookType = bookType,
texture = texture,
cost = cost,
reagent = reagent,
reagentId = reagentId,
}
end
if spellRank and not spells[bookType][spellName][spellRank] then
@@ -83,7 +79,7 @@ function CleveRoids.IndexSpells()
bookType = bookType,
texture = texture,
cost = cost,
reagent = reagent
reagentId = reagentId,
}
spells[bookType][spellName].highest = spells[bookType][spellName][spellRank]
end
@@ -94,6 +90,13 @@ function CleveRoids.IndexSpells()
if reagent then
CleveRoids.countedItemTypes[reagent] = true
elseif reagentId and Item then
Item:CreateFromItemID(reagentId):ContinueOnItemLoad(function()
local loadedName = C_Item.GetItemNameByID(reagentId)
if loadedName and loadedName ~= "" then
CleveRoids.countedItemTypes[loadedName] = true
end
end)
end
end
end
@@ -156,10 +159,9 @@ end
function CleveRoids.IndexEquippedItems()
local items = CleveRoids.Items or {}
for inventoryID = 0, 19 do
local link = GetInventoryItemLink("player", inventoryID)
if link then
local _, _, itemID = string.find(link, "item:(%d+)")
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)
@@ -199,10 +201,9 @@ function CleveRoids.IndexEquipSlot(inventoryID)
if not inventoryID then return end
local items = CleveRoids.Items or {}
local link = GetInventoryItemLink("player", inventoryID)
local itemID = GetInventoryItemID("player", inventoryID)
if link then
local _, _, itemID = string.find(link, "item:(%d+)")
if itemID then
local name, itemLink, _, _, itemType, itemSubType, _, _, texture = GetItemInfo(itemID)
if name then
local count = GetInventoryItemCount("player", inventoryID)
@@ -265,21 +266,17 @@ function CleveRoids.IndexItems()
-- PERFORMANCE: Local function references
local GetContainerNumSlots = GetContainerNumSlots
local GetContainerItemLink = GetContainerItemLink
local GetContainerItemInfo = GetContainerItemInfo
local GetInventoryItemLink = GetInventoryItemLink
local GetInventoryItemCount = GetInventoryItemCount
-- Scan bags (reverse order to prefer first stack)
for bagID = 0, NUM_BAG_SLOTS do
local numSlots = GetContainerNumSlots(bagID)
for slot = numSlots, 1, -1 do
local link = GetContainerItemLink(bagID, slot)
if link then
local _, _, itemID = string_find(link, "item:(%d+)")
-- PERFORMANCE: Try to extract name from link first to check for duplicates
local _, _, linkName = string_find(link, "%[(.+)%]")
local itemID = C_Container.GetContainerItemID(bagID, slot)
if itemID then
-- Decorated name for the duplicate fast-path (no link string built)
local linkName = C_Item.GetItemName({ bagID = bagID, slotIndex = slot })
local existing = linkName and items[linkName]
if existing then
@@ -317,13 +314,11 @@ function CleveRoids.IndexItems()
end
-- Scan equipped items
for inventoryID = 0, 19 do
local link = GetInventoryItemLink("player", inventoryID)
if link then
local _, _, itemID = string_find(link, "item:(%d+)")
-- PERFORMANCE: Try to extract name from link first
local _, _, linkName = string_find(link, "%[(.+)%]")
for inventoryID = 1, 19 do
local itemID = GetInventoryItemID("player", inventoryID)
if itemID then
-- Decorated name for the duplicate fast-path (no link string built)
local linkName = C_Item.GetItemName({ equipmentSlotIndex = inventoryID })
local existing = linkName and items[linkName]
if existing then
@@ -383,9 +378,9 @@ function CleveRoids.GetActionButtonInfo(slot)
if not actionType then return end
if actionType == "spell" and id then
local rank = GetSpellRecField(id, "rank")
local rank = C_Spell.GetSpellSubtext(id)
if rank == "" then rank = nil end
return "SPELL", id, GetSpellRecField(id, "name"), rank
return "SPELL", id, C_Spell.GetSpellName(id), rank
elseif actionType == "item" and id then
local item = CleveRoids.GetItem(id)
return "ITEM", id, (item and item.name)
@@ -477,10 +472,8 @@ local function makeInventoryItem(inventoryID, link, Items)
if not link then link = GetInventoryItemLink("player", inventoryID) end
if not link then return end
local _, _, itemID = string_find(link, "item:(%d+)")
itemID = itemID and tonumber(itemID) or nil
local name = itemID and GetItemInfo(itemID) or nil
local itemID = GetInventoryItemID("player", inventoryID)
local name = C_Item.GetItemName({ equipmentSlotIndex = inventoryID })
local texture = GetInventoryItemTexture("player", inventoryID)
local count = GetInventoryItemCount("player", inventoryID)
@@ -509,14 +502,12 @@ local function makeBagItem(bagID, slot, link, Items)
end
if not link then return end
local _, _, itemID = string_find(link, "item:(%d+)")
itemID = itemID and tonumber(itemID) or nil
local itemID = C_Container.GetContainerItemID(bagID, slot)
local name, _, _, _, _, _, _, _, texture = GetItemInfo(itemID)
local name = C_Item.GetItemName({bagID = bagID, slotIndex = slot})
local count = 0
local tex, itemCount = GetContainerItemInfo(bagID, slot)
local texture, itemCount = GetContainerItemInfo(bagID, slot)
if itemCount then count = itemCount end
if not texture then texture = tex end
local it = {
bagID = bagID,
@@ -574,9 +565,7 @@ function CleveRoids.GetItem(text)
for inv = 1, 19 do
local link = GetInventoryItemLink("player", inv)
if link then
local _, _, itemID = string_find(link, "item:(%d+)")
itemID = itemID and tonumber(itemID) or nil
local itemID = GetInventoryItemID("player", inv)
if qid and itemID and qid == itemID then
return makeInventoryItem(inv, link, Items)
elseif qname then
@@ -595,9 +584,7 @@ function CleveRoids.GetItem(text)
for slot = 1, slots do
local link = GetContainerItemLink(bag, slot)
if link then
local _, _, itemID = string_find(link, "item:(%d+)")
itemID = itemID and tonumber(itemID) or nil
local itemID = C_Container.GetContainerItemID(bag, slot)
if qid and itemID and qid == itemID then
return makeBagItem(bag, slot, link, Items)
elseif qname then
@@ -720,18 +707,16 @@ function CleveRoids.FindItemQuick(text)
if cached then
-- Validate: check if item is actually at the cached location
if cached.inventoryID then
local link = GetInventoryItemLink("player", cached.inventoryID)
if link then
local nm = GetNameFromLink(link)
if qid then
if GetInventoryItemID("player", cached.inventoryID) == qid then
cached._validated = true
return cached -- Cache is valid
end
else
local nm = C_Item.GetItemName({ equipmentSlotIndex = cached.inventoryID })
if nm and qname and string_lower(nm) == qname then
cached._validated = true
return cached -- Cache is valid
elseif qid then
local _, _, itemID = string_find(link, "item:(%d+)")
if itemID and tonumber(itemID) == qid then
cached._validated = true
return cached -- Cache is valid
end
end
end
-- Cache is stale - item not at cached equipped slot, invalidate
@@ -740,18 +725,16 @@ function CleveRoids.FindItemQuick(text)
Items[string_lower(cached.name)] = nil
end
elseif cached.bagID and cached.slot then
local link = GetContainerItemLink(cached.bagID, cached.slot)
if link then
local nm = GetNameFromLink(link)
if qid then
if C_Container.GetContainerItemID(cached.bagID, cached.slot) == qid then
cached._validated = true
return cached -- Cache is valid
end
else
local nm = C_Item.GetItemName({ bagID = cached.bagID, slotIndex = cached.slot })
if nm and qname and string_lower(nm) == qname then
cached._validated = true
return cached -- Cache is valid
elseif qid then
local _, _, itemID = string_find(link, "item:(%d+)")
if itemID and tonumber(itemID) == qid then
cached._validated = true
return cached -- Cache is valid
end
end
end
-- Cache is stale - item not at cached bag slot, invalidate
@@ -767,20 +750,17 @@ function CleveRoids.FindItemQuick(text)
for inv = 1, 19 do
local link = GetInventoryItemLink("player", inv)
if link then
local _, _, itemID = string_find(link, "item:(%d+)")
if itemID then
itemID = tonumber(itemID)
-- ID match: fast path
if qid and qid == itemID then
local itemID = GetInventoryItemID("player", inv)
-- ID match: fast path
if qid and qid == itemID then
return makeInventoryItem(inv, link, Items)
end
-- Name match: extract name from link (faster than GetItemInfo)
if qname then
local nm = GetNameFromLink(link)
if nm and string_lower(nm) == qname then
return makeInventoryItem(inv, link, Items)
end
-- Name match: extract name from link (faster than GetItemInfo)
if qname then
local nm = GetNameFromLink(link)
if nm and string_lower(nm) == qname then
return makeInventoryItem(inv, link, Items)
end
end
end
end
end
@@ -791,20 +771,17 @@ function CleveRoids.FindItemQuick(text)
for slot = 1, slots do
local link = GetContainerItemLink(bag, slot)
if link then
local _, _, itemID = string_find(link, "item:(%d+)")
if itemID then
itemID = tonumber(itemID)
-- ID match: fast path
if qid and qid == itemID then
local itemID = C_Container.GetContainerItemID(bag, slot)
-- ID match: fast path
if qid and qid == itemID then
return makeBagItem(bag, slot, link, Items)
end
-- Name match: extract name from link (faster than GetItemInfo)
if qname then
local nm = GetNameFromLink(link)
if nm and string_lower(nm) == qname then
return makeBagItem(bag, slot, link, Items)
end
-- Name match: extract name from link (faster than GetItemInfo)
if qname then
local nm = GetNameFromLink(link)
if nm and string_lower(nm) == qname then
return makeBagItem(bag, slot, link, Items)
end
end
end
end
end
@@ -818,25 +795,19 @@ end
function CleveRoids.IsItemEquipped(text, inventoryId)
if not text or not inventoryId then return false end
local link = GetInventoryItemLink("player", inventoryId)
if not link then return false end
local _, _, currentID = string_find(link, "item:(%d+)")
local currentID = GetInventoryItemID("player", inventoryId)
if not currentID then return false end
-- Check by ID (fast path)
local textId = tonumber(text)
if textId and textId == tonumber(currentID) then
if textId and textId == currentID then
return true
end
-- Check by name - extract from link instead of GetItemInfo for performance
local currentName = GetNameFromLink(link)
if currentName then
local textLower = string_lower(text)
if string_lower(currentName) == textLower then
return true
end
-- Check by name (decorated, so suffixed gear still matches)
local currentName = C_Item.GetItemName({ equipmentSlotIndex = inventoryId })
if currentName and string_lower(currentName) == string_lower(text) then
return true
end
return false
+4
View File
@@ -651,6 +651,8 @@ local function validateConditional(conditional, args, action)
combo = true,
hp = true, myhp = true, rawhp = true, myrawhp = true,
power = true, mypower = true, rawpower = true, myrawpower = true,
mana = true, mymana = true, rage = true, myrage = true,
energy = true, myenergy = true,
hplost = true, myhplost = true,
powerlost = true, mypowerlost = true,
stat = true,
@@ -678,6 +680,8 @@ local function validateConditional(conditional, args, action)
-- Check operator syntax for numeric comparisons
if args and type(args) == "string" then
local hasHpOrPower = safeStringFind(baseCond, "hp") or safeStringFind(baseCond, "power") or
safeStringFind(baseCond, "mana") or safeStringFind(baseCond, "energy") or
safeStringFind(baseCond, "rage") or
safeStringFind(baseCond, "combo") or baseCond == "stat"
if hasHpOrPower then
local hasOperator = safeStringFind(args, "[<>=~]+")
+2 -41
View File
@@ -71,7 +71,6 @@
Spell Miss Events (v2.31+):
- SPELL_MISS_SELF / SPELL_MISS_OTHER - Spell miss/resist/immune/dodge/etc.
- GetSpellPower([mode]) - Player mod damage done for all 7 schools
Aura Event State Parameter (v2.32+):
- Buff/debuff events include 7th `state` parameter (0=added, 1=removed, 2=modified)
@@ -364,9 +363,8 @@ API.VERSION_REQUIREMENTS = {
["AuraDurationEvents"] = { 2, 30, 0 },
["GetPlayerAuraDuration"] = { 2, 30, 0, "GetPlayerAuraDuration" },
-- v2.31+ - Spell miss events and spell power query
-- v2.31+ - Spell miss events
["SpellMissEvents"] = { 2, 31, 0 }, -- SPELL_MISS_SELF/OTHER events
["GetSpellPower"] = { 2, 31, 0, "GetSpellPower" },
-- v2.32+ - Aura event state parameter and stack removal fix
["AuraEventState"] = { 2, 32, 0 },
@@ -575,9 +573,8 @@ local function InitializeFeatures()
f.hasAuraDurationEvents = API.HasFeature("AuraDurationEvents")
f.hasGetPlayerAuraDuration = API.HasFeature("GetPlayerAuraDuration")
-- v2.31+ Spell miss events and spell power
-- v2.31+ Spell miss events
f.hasSpellMissEvents = API.HasFeature("SpellMissEvents")
f.hasGetSpellPower = API.HasFeature("GetSpellPower")
-- v2.32+ Aura event state parameter
f.hasAuraEventState = API.HasFeature("AuraEventState")
@@ -3387,15 +3384,6 @@ API.MISS_INFO = {
-- SPELL POWER QUERY (v2.31+)
--------------------------------------------------------------------------------
-- Get spell power for all 7 damage schools (v2.31+)
-- mode: optional mode parameter passed to GetSpellPower
-- Returns: physical, holy, fire, nature, frost, shadow, arcane (or nil if unavailable)
function API.GetSpellPower(mode)
if not API.features.hasGetSpellPower or not _G.GetSpellPower then
return nil, nil, nil, nil, nil, nil, nil
end
return _G.GetSpellPower(mode)
end
-- Get duration of a spell in milliseconds (v2.38+)
-- For channeling spells: returns the channel duration.
@@ -3504,32 +3492,5 @@ function API.GetUnitMaxHealth(unitToken)
return UnitHealthMax(unitToken)
end
-- powerType: nil=current, 0=mana, 1=rage, 2=focus, 3=energy
-- GetUnitField uses power1-power4 fields
local POWER_FIELDS = { [0] = "power1", [1] = "power2", [2] = "power3", [3] = "power4" }
local MAX_POWER_FIELDS = { [0] = "maxPower1", [1] = "maxPower2", [2] = "maxPower3", [3] = "maxPower4" }
function API.GetUnitPower(unitToken, powerType)
if API.features.hasGetUnitField and GetUnitField and powerType then
local field = POWER_FIELDS[powerType]
if field then
local val = GetUnitField(unitToken, field)
if val then return val end
end
end
return UnitMana(unitToken)
end
function API.GetUnitMaxPower(unitToken, powerType)
if API.features.hasGetUnitField and GetUnitField and powerType then
local field = MAX_POWER_FIELDS[powerType]
if field then
local val = GetUnitField(unitToken, field)
if val then return val end
end
end
return UnitManaMax(unitToken)
end
-- Expose API globally for other addons
_G.CleveRoidsNampowerAPI = API
+180 -215
View File
File diff suppressed because it is too large Load Diff
+9 -10
View File
@@ -49,16 +49,15 @@ API references are line numbers into `C:\Git\ClassicAPI\docs\API.md`.
- class-aware `[dispellable]` (only types this character can actually remove).
- optional name filtering on the type keywords (e.g. `[magic:Polymorph]`).
### 2. `C_Item.GetWeaponEnchantInfo()` — temp-enchant IDs
- **API:** `API.md:7159` — returns 12-tuple including `enchantID` for main/off/ranged.
- **Unlocks:** detect *which* temp enchant (poison/oil/sharpening stone) is
applied to a weapon, not just that one exists.
- **Naming:** `[poison]` is now taken by the target dispel-type conditional
(slice 1 above). Use weapon-specific keywords for this — e.g.
`[mhenchant:<id>]` / `[ohenchant:<id>]` (or `[mhpoison:<id>]`/`[ohpoison:<id>]`),
not a bare `[poison]`.
- Vanilla's global only reports presence; this is a genuinely new capability
(rogue/shaman/enhance).
### 2. ~~`C_Item.GetWeaponEnchantInfo()` — temp-enchant IDs~~ — DONE
- Shipped as `[mhenchant]` / `[ohenchant]` (+ `no` variants). Matches the applied
temp enchant by SpellItemEnchantment ID *or* localized name — the name path
resolves via `C_Item.GetEnchantInfo(id).name` (the ID→name table this doc
assumed we lacked), so no tooltip scan. Wrappers `ClassicAPI.GetWeaponEnchant`
/ `GetEnchantName`; `ValidateWeaponImbue` now reads through the former.
- Bare = any temp enchant; OR-lists supported (`[mhenchant:2823/Deadly_Poison]`).
- Follow-up (optional): route `[mhimbue:Name]`'s match through `GetEnchantName`
too, retiring the green-text tooltip scan in `CheckWeaponImbueByName`.
### 3. `GetUnitSpeed(unit)` + `IsFalling()` / `IsSwimming()`
- **API:** `API.md:8312`, `API.md:7629`.