cursive mouseover support, applymain and applyoff slashcommands, nampower feature update with incominghit, lastswing, buffcap, debuffcap conditionals

This commit is contained in:
Jrc13245
2026-01-12 17:13:40 -05:00
parent 5af6296550
commit 7d0dec2a2b
7 changed files with 943 additions and 79 deletions
+683 -67
View File
@@ -759,6 +759,392 @@ threatFrame:SetScript("OnEvent", function()
end
end)
-- ============================================================================
-- AUTO-ATTACK EVENT TRACKING (Nampower v2.24+)
-- ============================================================================
-- Tracks player's melee swings and incoming attacks for conditionals:
-- [lastswing:crit/glancing/miss/dodge/parry/blocked/offhand]
-- [incominghit:crit/crushing/dodge/parry/blocked]
-- [buffcapped] / [debuffcapped]
-- Requires NP_EnableAutoAttackEvents=1 and NP_EnableAuraCastEvents=1 CVars
-- State tracking for player's outgoing melee swings
CleveRoids.LastSwing = {
timestamp = 0, -- GetTime() when swing occurred
damage = 0, -- Total damage dealt
hitInfo = 0, -- HitInfo bitfield
victimState = 0, -- VictimState enum
blockedAmount = 0, -- Amount blocked
absorbAmount = 0, -- Amount absorbed
resistAmount = 0, -- Amount resisted
targetGuid = nil, -- GUID of target hit
}
-- State tracking for incoming attacks on player
CleveRoids.LastIncomingHit = {
timestamp = 0,
damage = 0,
hitInfo = 0,
victimState = 0,
blockedAmount = 0,
absorbAmount = 0,
resistAmount = 0,
attackerGuid = nil,
}
-- Aura cap status tracking (from AURA_CAST events)
-- For player: 32 buff slots, 16 debuff slots
-- For enemy NPCs: 16 debuff slots + 32 overflow into buff slots = 48 total visual
CleveRoids.AuraCapStatus = {
-- Player aura cap status
playerBuffCapped = false,
playerDebuffCapped = false,
playerLastUpdate = 0,
-- Target aura cap status (tracked per-GUID)
targetCapStatus = {}, -- [guid] = { buffCapped, debuffCapped, timestamp }
}
-- HitInfo bitfield values (from NampowerAPI.lua, duplicated for local access)
-- Converted to decimal for Lua 5.0 compatibility (no hex literals)
local HITINFO_MISS = 16 -- 0x10
local HITINFO_CRITICALHIT = 128 -- 0x80
local HITINFO_GLANCING = 16384 -- 0x4000
local HITINFO_CRUSHING = 32768 -- 0x8000
local HITINFO_LEFTSWING = 4 -- 0x4 (Off-hand attack)
-- VictimState values
local VICTIMSTATE_DODGE = 2
local VICTIMSTATE_PARRY = 3
local VICTIMSTATE_BLOCKS = 5
-- Aura cap status bitfield
local AURA_CAP_BUFF_FULL = 1
local AURA_CAP_DEBUFF_FULL = 2
-- Helper: Check if a HitInfo bitfield contains a specific flag
local function HasHitFlag(hitInfo, flag)
if not hitInfo or not flag then return false end
if bit and bit.band then
return bit.band(hitInfo, flag) ~= 0
end
-- Fallback for environments without bit library
return false
end
-- Process AUTO_ATTACK_OTHER event (player attacking something, or other units)
local function OnAutoAttackOther(attackerGuid, targetGuid, totalDamage, hitInfo, victimState,
subDamageCount, blockedAmount, totalAbsorb, totalResist)
-- Check if player is the attacker
local _, playerGuid = UnitExists("player")
if not playerGuid or attackerGuid ~= playerGuid then
return -- Not player's attack, ignore
end
CleveRoids.LastSwing.timestamp = GetTime()
CleveRoids.LastSwing.damage = totalDamage or 0
CleveRoids.LastSwing.hitInfo = hitInfo or 0
CleveRoids.LastSwing.victimState = victimState or 0
CleveRoids.LastSwing.blockedAmount = blockedAmount or 0
CleveRoids.LastSwing.absorbAmount = totalAbsorb or 0
CleveRoids.LastSwing.resistAmount = totalResist or 0
CleveRoids.LastSwing.targetGuid = targetGuid
end
-- Process AUTO_ATTACK_SELF event (player being attacked)
local function OnAutoAttackSelf(attackerGuid, targetGuid, totalDamage, hitInfo, victimState,
subDamageCount, blockedAmount, totalAbsorb, totalResist)
CleveRoids.LastIncomingHit.timestamp = GetTime()
CleveRoids.LastIncomingHit.damage = totalDamage or 0
CleveRoids.LastIncomingHit.hitInfo = hitInfo or 0
CleveRoids.LastIncomingHit.victimState = victimState or 0
CleveRoids.LastIncomingHit.blockedAmount = blockedAmount or 0
CleveRoids.LastIncomingHit.absorbAmount = totalAbsorb or 0
CleveRoids.LastIncomingHit.resistAmount = totalResist or 0
CleveRoids.LastIncomingHit.attackerGuid = attackerGuid
end
-- Process AURA_CAST events for cap status tracking
-- Parameters: spellId, casterGuid, targetGuid, effect, effectAuraName,
-- effectAmplitude, effectMiscValue, durationMs, auraCapStatus
local function OnAuraCastSelf(spellId, casterGuid, targetGuid, effect, effectAuraName,
effectAmplitude, effectMiscValue, durationMs, auraCapStatus)
if not auraCapStatus then return end
local now = GetTime()
CleveRoids.AuraCapStatus.playerLastUpdate = now
-- Check buff bar full (bit 1)
CleveRoids.AuraCapStatus.playerBuffCapped = HasHitFlag(auraCapStatus, AURA_CAP_BUFF_FULL)
-- Check debuff bar full (bit 2)
CleveRoids.AuraCapStatus.playerDebuffCapped = HasHitFlag(auraCapStatus, AURA_CAP_DEBUFF_FULL)
end
local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAuraName,
effectAmplitude, effectMiscValue, durationMs, auraCapStatus)
if not auraCapStatus or not targetGuid then return end
local now = GetTime()
-- Store cap status for this target GUID
CleveRoids.AuraCapStatus.targetCapStatus[targetGuid] = {
buffCapped = HasHitFlag(auraCapStatus, AURA_CAP_BUFF_FULL),
debuffCapped = HasHitFlag(auraCapStatus, AURA_CAP_DEBUFF_FULL),
timestamp = now,
}
-- Cleanup old entries (older than 60 seconds) to prevent memory leak
-- Only clean up every 100 entries to avoid performance impact
local count = 0
for guid, data in pairs(CleveRoids.AuraCapStatus.targetCapStatus) do
count = count + 1
if now - data.timestamp > 60 then
CleveRoids.AuraCapStatus.targetCapStatus[guid] = nil
end
end
end
-- Create frame to listen for Nampower auto-attack and aura events
local autoAttackFrame = CreateFrame("Frame", "CleveRoidsAutoAttackFrame")
-- Register for events on PLAYER_ENTERING_WORLD (after CVars are applied)
autoAttackFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
autoAttackFrame:SetScript("OnEvent", function()
if event == "PLAYER_ENTERING_WORLD" then
-- Check if Nampower v2.24+ is available for auto-attack events
local API = CleveRoids.NampowerAPI
if API and API.features and API.features.hasAutoAttackEvents then
-- Register for auto-attack events (requires NP_EnableAutoAttackEvents=1)
this:RegisterEvent("AUTO_ATTACK_OTHER")
this:RegisterEvent("AUTO_ATTACK_SELF")
end
-- Check for aura cast events (v2.20+, requires NP_EnableAuraCastEvents=1)
if API and API.features and API.features.hasAuraCastEvents then
this:RegisterEvent("AURA_CAST_ON_SELF")
this:RegisterEvent("AURA_CAST_ON_OTHER")
end
elseif event == "AUTO_ATTACK_OTHER" then
-- arg1=attackerGuid, arg2=targetGuid, arg3=totalDamage, arg4=hitInfo, arg5=victimState,
-- arg6=subDamageCount, arg7=blockedAmount, arg8=totalAbsorb, arg9=totalResist
OnAutoAttackOther(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
elseif event == "AUTO_ATTACK_SELF" then
OnAutoAttackSelf(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
elseif event == "AURA_CAST_ON_SELF" then
-- arg1=spellId, arg2=casterGuid, arg3=targetGuid, arg4=effect, arg5=effectAuraName,
-- arg6=effectAmplitude, arg7=effectMiscValue, arg8=durationMs, arg9=auraCapStatus
OnAuraCastSelf(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
elseif event == "AURA_CAST_ON_OTHER" then
OnAuraCastOther(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
end
end)
-- ============================================================================
-- VALIDATION FUNCTIONS FOR AUTO-ATTACK CONDITIONALS
-- ============================================================================
-- Validate lastswing conditional
-- swingType: "crit", "glancing", "miss", "dodge", "parry", "blocked", "offhand", or time comparison
function CleveRoids.ValidateLastSwing(swingType, operator, amount)
local swing = CleveRoids.LastSwing
if swing.timestamp == 0 then
return false -- No swing recorded yet
end
local swingTypeLower = swingType and string.lower(swingType)
-- Time-based check (e.g., [lastswing:<2] = within last 2 seconds)
if operator and amount then
local elapsed = GetTime() - swing.timestamp
if CleveRoids.operators[operator] then
return CleveRoids.comparators[operator](elapsed, amount)
end
return false
end
-- Type-based checks
if swingTypeLower == "crit" or swingTypeLower == "critical" then
return HasHitFlag(swing.hitInfo, HITINFO_CRITICALHIT)
elseif swingTypeLower == "glancing" then
return HasHitFlag(swing.hitInfo, HITINFO_GLANCING)
elseif swingTypeLower == "miss" then
return HasHitFlag(swing.hitInfo, HITINFO_MISS)
elseif swingTypeLower == "dodge" or swingTypeLower == "dodged" then
return swing.victimState == VICTIMSTATE_DODGE
elseif swingTypeLower == "parry" or swingTypeLower == "parried" then
return swing.victimState == VICTIMSTATE_PARRY
elseif swingTypeLower == "blocked" or swingTypeLower == "block" then
return swing.victimState == VICTIMSTATE_BLOCKS or swing.blockedAmount > 0
elseif swingTypeLower == "offhand" or swingTypeLower == "oh" then
return HasHitFlag(swing.hitInfo, HITINFO_LEFTSWING)
elseif swingTypeLower == "mainhand" or swingTypeLower == "mh" then
return not HasHitFlag(swing.hitInfo, HITINFO_LEFTSWING)
elseif swingTypeLower == "hit" then
-- A successful hit (not miss, not dodged, not parried)
return not HasHitFlag(swing.hitInfo, HITINFO_MISS) and
swing.victimState ~= VICTIMSTATE_DODGE and
swing.victimState ~= VICTIMSTATE_PARRY
end
-- Default: check if any swing was recorded recently (within 5 seconds)
return (GetTime() - swing.timestamp) < 5
end
-- Validate incominghit conditional
function CleveRoids.ValidateIncomingHit(hitType, operator, amount)
local hit = CleveRoids.LastIncomingHit
if hit.timestamp == 0 then
return false -- No incoming hit recorded
end
local hitTypeLower = hitType and string.lower(hitType)
-- Time-based check
if operator and amount then
local elapsed = GetTime() - hit.timestamp
if CleveRoids.operators[operator] then
return CleveRoids.comparators[operator](elapsed, amount)
end
return false
end
-- Type-based checks
if hitTypeLower == "crit" or hitTypeLower == "critical" then
return HasHitFlag(hit.hitInfo, HITINFO_CRITICALHIT)
elseif hitTypeLower == "crushing" then
return HasHitFlag(hit.hitInfo, HITINFO_CRUSHING)
elseif hitTypeLower == "glancing" then
return HasHitFlag(hit.hitInfo, HITINFO_GLANCING)
elseif hitTypeLower == "miss" or hitTypeLower == "missed" then
return HasHitFlag(hit.hitInfo, HITINFO_MISS)
elseif hitTypeLower == "dodge" or hitTypeLower == "dodged" then
return hit.victimState == VICTIMSTATE_DODGE
elseif hitTypeLower == "parry" or hitTypeLower == "parried" then
return hit.victimState == VICTIMSTATE_PARRY
elseif hitTypeLower == "blocked" or hitTypeLower == "block" then
return hit.victimState == VICTIMSTATE_BLOCKS or hit.blockedAmount > 0
elseif hitTypeLower == "hit" then
-- A successful incoming hit (not miss, not dodged, not parried by us)
return not HasHitFlag(hit.hitInfo, HITINFO_MISS) and
hit.victimState ~= VICTIMSTATE_DODGE and
hit.victimState ~= VICTIMSTATE_PARRY
end
-- Default: check if any incoming hit was recorded recently
return (GetTime() - hit.timestamp) < 5
end
-- Check if player's buff bar is capped (32 slots)
function CleveRoids.IsPlayerBuffCapped()
-- First check from AURA_CAST events (most accurate)
if CleveRoids.AuraCapStatus.playerLastUpdate > 0 then
return CleveRoids.AuraCapStatus.playerBuffCapped
end
-- Fallback: count player buffs manually
local count = 0
for i = 0, 31 do
if GetPlayerBuffTexture(GetPlayerBuff(i, "HELPFUL")) then
count = count + 1
end
end
return count >= 32
end
-- Check if player's debuff bar is capped (16 slots)
function CleveRoids.IsPlayerDebuffCapped()
if CleveRoids.AuraCapStatus.playerLastUpdate > 0 then
return CleveRoids.AuraCapStatus.playerDebuffCapped
end
-- Fallback: count player debuffs
local count = 0
for i = 0, 15 do
if GetPlayerBuffTexture(GetPlayerBuff(i, "HARMFUL")) then
count = count + 1
end
end
return count >= 16
end
-- Check if target's debuff bar is capped
-- For NPCs: 16 debuff slots + 32 overflow = 48 total visual debuff capacity
function CleveRoids.IsTargetDebuffCapped(unit)
unit = unit or "target"
if not UnitExists(unit) then return false end
-- Check cached AURA_CAST data by GUID
local _, guid = UnitExists(unit)
if guid then
local capData = CleveRoids.AuraCapStatus.targetCapStatus[guid]
if capData and (GetTime() - capData.timestamp) < 30 then
return capData.debuffCapped
end
end
-- Fallback: count debuffs on target manually
-- For NPCs: check up to 48 slots (16 debuff + 32 overflow in buff slots)
if not CleveRoids.hasSuperwow then
-- Without SuperWoW, can only see 16 debuff slots
local count = 0
for i = 1, 16 do
if UnitDebuff(unit, i) then
count = count + 1
else
break
end
end
return count >= 16
end
-- With SuperWoW: count all debuffs including overflow
local debuffCount = 0
-- Count regular debuff slots (1-16, dense)
for i = 1, 16 do
local texture, _, _, spellId = UnitDebuff(unit, i)
if texture then
debuffCount = debuffCount + 1
else
break -- Dense, stop at first nil
end
end
-- Count overflow debuffs in buff slots (sparse, check all 32)
-- Overflow debuffs appear in buff slots but are filtered by UnitDebuff
-- We need to use libdebuff or check buff slots for debuff-like effects
-- For simplicity, consider 16 regular debuffs = capped for most use cases
return debuffCount >= 16
end
-- Check if target's buff bar is capped (32 slots for NPCs)
function CleveRoids.IsTargetBuffCapped(unit)
unit = unit or "target"
if not UnitExists(unit) then return false end
local _, guid = UnitExists(unit)
if guid then
local capData = CleveRoids.AuraCapStatus.targetCapStatus[guid]
if capData and (GetTime() - capData.timestamp) < 30 then
return capData.buffCapped
end
end
-- Fallback: count buffs
local count = 0
for i = 1, 32 do
if UnitBuff(unit, i) then
count = count + 1
else
break
end
end
return count >= 32
end
-- ============================================================================
-- pfUI debuff time helper (Vanilla 1.12.1 / Lua 5.0 safe)
@@ -2353,10 +2739,10 @@ function CleveRoids.ValidatePlayerDebuff(args)
return CleveRoids.ValidateAura("player", args, false)
end
function CleveRoids.ValidateWeaponImbue(slot, imbueName)
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
@@ -2367,25 +2753,120 @@ function CleveRoids.ValidateWeaponImbue(slot, imbueName)
expiration = offExpiration
charges = offCharges
end
-- Only consider temporary enchants (with time or charges)
-- This filters out permanent enchants like Crusader, Lifestealing, etc.
local hasTemporaryEnchant = hasEnchant and (expiration and expiration > 0 or charges and charges > 0)
-- If no specific imbue requested, return temporary enchant status
if not imbueName or imbueName == "" then
-- Convert expiration from milliseconds to seconds
local remaining = expiration and (expiration / 1000) or -1
local stacks = charges or 0
-- Normalize args to table format
if type(args) ~= "table" then
args = { name = args }
end
local imbueName = args.name
-- If no specific imbue requested and no comparison operators, return temporary enchant status
if (not imbueName or imbueName == "") and not args.operator and not args.comparisons then
return hasTemporaryEnchant
end
-- If no temporary enchant, don't bother scanning
-- If no temporary enchant, don't bother with further checks
if not hasTemporaryEnchant then
return false
end
-- For specific imbue names, scan tooltip to match the name
-- BUT only check lines that have time markers (temporary enchants)
-- This prevents matching weapon stats like "Equip: ... critical strike ..."
-- If we have a specific imbue name, verify it via tooltip scan
if imbueName and imbueName ~= "" then
local nameFound = CleveRoids.CheckWeaponImbueByName(slot, imbueName)
if not nameFound then
return false -- Specific imbue not found
end
end
-- Handle numeric comparisons (time remaining or charges)
local ops = CleveRoids.operators
-- Handle multi-comparison (e.g., >60&<300)
if args.comparisons and type(args.comparisons) == "table" then
-- ALL comparisons must pass (AND logic)
for _, comp in ipairs(args.comparisons) do
if not ops[comp.operator] then
return false -- Invalid operator
end
local value_to_check
if comp.checkStacks then
value_to_check = stacks
else
value_to_check = remaining
end
if not CleveRoids.comparators[comp.operator](value_to_check, comp.amount) then
return false -- One comparison failed
end
end
return true -- All comparisons passed
end
-- Single comparison (backward compatibility)
if not args.amount and not args.operator and not args.checkStacks then
return true -- Name matched (or no name required), no numeric check needed
elseif not args.checkStacks and args.amount and ops[args.operator] then
-- Time remaining check
return CleveRoids.comparators[args.operator](remaining, args.amount)
elseif args.amount and args.checkStacks and ops[args.operator] then
-- Charge count check
return CleveRoids.comparators[args.operator](stacks, args.amount)
else
return false
end
end
-- Helper function: Parse imbue conditional arguments into a normalized args table
-- Handles: boolean, string, or table with name/operator/amount/checkStacks/comparisons
function CleveRoids.ParseImbueArgs(value, conditionals)
-- Boolean true means check for any imbue
if value == true then
return nil
end
-- Simple string means just check for name
if type(value) == "string" then
return { name = value }
end
-- Table format - could be array of values or parsed args with operator
if type(value) == "table" then
-- Check if it's already a parsed args table (has operator or comparisons)
if value.operator or value.comparisons or value.checkStacks then
return value
end
-- It's an array of values - use the first one
if table.getn(value) > 0 then
local first = value[1]
-- First element could be a string or a parsed args table
if type(first) == "string" then
return { name = first }
elseif type(first) == "table" then
return first -- Already parsed (has name, operator, amount, etc.)
end
end
end
return nil -- Default: check for any imbue
end
-- Helper function: Check if specific imbue name is on weapon via tooltip scan
function CleveRoids.CheckWeaponImbueByName(slot, imbueName)
if not imbueName or imbueName == "" then
return true -- No name to check
end
-- Create tooltip scanner if needed
if not CleveRoidsTooltip then
CreateFrame("GameTooltip", "CleveRoidsTooltip", nil, "GameTooltipTemplate")
@@ -3520,6 +4001,36 @@ CleveRoids.Keywords = {
return not CleveRoids.IsTargetInGroupType(unit, "raid")
end,
-- [tag] - target is tapped (tagged) by anyone
tag = function(conditionals)
return conditionals.target and UnitIsTapped(conditionals.target)
end,
-- [notag] - target is not tapped
notag = function(conditionals)
return conditionals.target and not UnitIsTapped(conditionals.target)
end,
-- [mytag] - target is tapped by the player
mytag = function(conditionals)
return conditionals.target and UnitIsTappedByPlayer(conditionals.target)
end,
-- [nomytag] - target is not tapped by the player
nomytag = function(conditionals)
return conditionals.target and not UnitIsTappedByPlayer(conditionals.target)
end,
-- [othertag] - target is tapped by someone else (not the player)
othertag = function(conditionals)
return conditionals.target and UnitIsTapped(conditionals.target) and not UnitIsTappedByPlayer(conditionals.target)
end,
-- [noothertag] - target is not tapped by someone else (not tapped, or tapped by player)
noothertag = function(conditionals)
return conditionals.target and (not UnitIsTapped(conditionals.target) or UnitIsTappedByPlayer(conditionals.target))
end,
-- [group] or [group:party] or [group:raid] or [group:party/raid]
-- Checks if the PLAYER is in a group (not unit membership)
group = function(conditionals)
@@ -4489,72 +5000,31 @@ CleveRoids.Keywords = {
end, conditionals, "nomybuffcount")
end,
-- [mhimbue] - Check main hand has temporary imbue (poison, oil, sharpening stone, etc.)
-- [mhimbue:Name] - Check for specific imbue name
-- [mhimbue:<300] - Check imbue time remaining < 300 seconds
-- [mhimbue:Name<300] - Specific imbue with time check
-- [mhimbue:>#5] - Check imbue charges > 5
mhimbue = function(conditionals)
local imbueName = nil
-- Case 1: conditionals.mhimbue is a string (e.g., [mhimbue]Frostbrand)
if type(conditionals.mhimbue) == "string" then
imbueName = conditionals.mhimbue
-- Case 2: conditionals.mhimbue is a table (e.g., [mhimbue:Frostbrand])
elseif type(conditionals.mhimbue) == "table" and table.getn(conditionals.mhimbue) > 0 then
imbueName = conditionals.mhimbue[1] -- Use first value
-- Case 3: Boolean true means check for any imbue
elseif conditionals.mhimbue == true then
imbueName = nil -- Check for existence only
end
return CleveRoids.ValidateWeaponImbue("mh", imbueName)
local args = CleveRoids.ParseImbueArgs(conditionals.mhimbue, conditionals)
return CleveRoids.ValidateWeaponImbue("mh", args)
end,
nomhimbue = function(conditionals)
local imbueName = nil
-- Case 1: conditionals.nomhimbue is a string
if type(conditionals.nomhimbue) == "string" then
imbueName = conditionals.nomhimbue
-- Case 2: conditionals.nomhimbue is a table
elseif type(conditionals.nomhimbue) == "table" and table.getn(conditionals.nomhimbue) > 0 then
imbueName = conditionals.nomhimbue[1]
-- Case 3: Boolean true
elseif conditionals.nomhimbue == true then
imbueName = nil
end
return not CleveRoids.ValidateWeaponImbue("mh", imbueName)
local args = CleveRoids.ParseImbueArgs(conditionals.nomhimbue, conditionals)
return not CleveRoids.ValidateWeaponImbue("mh", args)
end,
-- [ohimbue] - Check off hand has temporary imbue
-- Same syntax as mhimbue
ohimbue = function(conditionals)
local imbueName = nil
-- Case 1: conditionals.ohimbue is a string
if type(conditionals.ohimbue) == "string" then
imbueName = conditionals.ohimbue
-- Case 2: conditionals.ohimbue is a table
elseif type(conditionals.ohimbue) == "table" and table.getn(conditionals.ohimbue) > 0 then
imbueName = conditionals.ohimbue[1]
-- Case 3: Boolean true
elseif conditionals.ohimbue == true then
imbueName = nil
end
return CleveRoids.ValidateWeaponImbue("oh", imbueName)
local args = CleveRoids.ParseImbueArgs(conditionals.ohimbue, conditionals)
return CleveRoids.ValidateWeaponImbue("oh", args)
end,
noohimbue = function(conditionals)
local imbueName = nil
-- Case 1: conditionals.noohimbue is a string
if type(conditionals.noohimbue) == "string" then
imbueName = conditionals.noohimbue
-- Case 2: conditionals.noohimbue is a table
elseif type(conditionals.noohimbue) == "table" and table.getn(conditionals.noohimbue) > 0 then
imbueName = conditionals.noohimbue[1]
-- Case 3: Boolean true
elseif conditionals.noohimbue == true then
imbueName = nil
end
return not CleveRoids.ValidateWeaponImbue("oh", imbueName)
local args = CleveRoids.ParseImbueArgs(conditionals.noohimbue, conditionals)
return not CleveRoids.ValidateWeaponImbue("oh", args)
end,
immune = function(conditionals)
@@ -5295,6 +5765,152 @@ CleveRoids.Keywords = {
local normalizedForbidden = string.lower(CleveRoids.NormalizeName(forbiddenName))
return unitNameLower ~= normalizedForbidden
end, conditionals, "noname")
end,
-- ========================================================================
-- AUTO-ATTACK CONDITIONALS (Nampower v2.24+)
-- ========================================================================
-- Requires NP_EnableAutoAttackEvents=1 CVar
-- [lastswing:type] - Check result of player's last melee swing
-- Types: crit, glancing, miss, dodge, parry, blocked, offhand/oh, mainhand/mh, hit
-- Time check: [lastswing:<2] = last swing was within 2 seconds
-- Multi-value: [lastswing:crit/glancing] = was crit OR glancing (OR logic)
-- Examples: [lastswing:dodge] [@target,lastswing:crit] [lastswing:offhand]
lastswing = function(conditionals)
-- Boolean form [lastswing] - any recent swing
if not conditionals.lastswing or
conditionals.lastswing == true or
(type(conditionals.lastswing) == "table" and table.getn(conditionals.lastswing) == 0) then
return CleveRoids.ValidateLastSwing(nil, nil, nil)
end
-- Check for specific swing type(s) - OR logic
return Or(conditionals.lastswing, function(args)
if type(args) == "string" then
return CleveRoids.ValidateLastSwing(args, nil, nil)
elseif type(args) == "table" then
return CleveRoids.ValidateLastSwing(args.name, args.operator, args.amount)
end
return false
end)
end,
-- [nolastswing:type] - Check that last swing was NOT a specific type
-- Uses AND logic: [nolastswing:crit/glancing] = was NOT crit AND NOT glancing
nolastswing = function(conditionals)
-- Boolean form [nolastswing] - no recent swing
if not conditionals.nolastswing or
conditionals.nolastswing == true or
(type(conditionals.nolastswing) == "table" and table.getn(conditionals.nolastswing) == 0) then
return not CleveRoids.ValidateLastSwing(nil, nil, nil)
end
-- Check that swing was NOT any of the specified types - AND logic
return NegatedMulti(conditionals.nolastswing, function(args)
if type(args) == "string" then
return not CleveRoids.ValidateLastSwing(args, nil, nil)
elseif type(args) == "table" then
return not CleveRoids.ValidateLastSwing(args.name, args.operator, args.amount)
end
return true
end, conditionals, "nolastswing")
end,
-- [incominghit:type] - Check result of last attack received by player
-- Types: crit, crushing, glancing, miss, dodge, parry, blocked, hit
-- Time check: [incominghit:<1] = received hit within last 1 second
-- Examples: [incominghit:crushing] [incominghit:crit/crushing] [incominghit:dodge]
incominghit = function(conditionals)
-- Boolean form [incominghit] - any recent incoming hit
if not conditionals.incominghit or
conditionals.incominghit == true or
(type(conditionals.incominghit) == "table" and table.getn(conditionals.incominghit) == 0) then
return CleveRoids.ValidateIncomingHit(nil, nil, nil)
end
-- Check for specific hit type(s) - OR logic
return Or(conditionals.incominghit, function(args)
if type(args) == "string" then
return CleveRoids.ValidateIncomingHit(args, nil, nil)
elseif type(args) == "table" then
return CleveRoids.ValidateIncomingHit(args.name, args.operator, args.amount)
end
return false
end)
end,
-- [noincominghit:type] - Check that last incoming hit was NOT a specific type
noincominghit = function(conditionals)
-- Boolean form [noincominghit] - no recent incoming hit
if not conditionals.noincominghit or
conditionals.noincominghit == true or
(type(conditionals.noincominghit) == "table" and table.getn(conditionals.noincominghit) == 0) then
return not CleveRoids.ValidateIncomingHit(nil, nil, nil)
end
-- Check that hit was NOT any of the specified types - AND logic
return NegatedMulti(conditionals.noincominghit, function(args)
if type(args) == "string" then
return not CleveRoids.ValidateIncomingHit(args, nil, nil)
elseif type(args) == "table" then
return not CleveRoids.ValidateIncomingHit(args.name, args.operator, args.amount)
end
return true
end, conditionals, "noincominghit")
end,
-- ========================================================================
-- AURA CAP CONDITIONALS (Nampower v2.20+ with AURA_CAST events)
-- ========================================================================
-- Requires NP_EnableAuraCastEvents=1 CVar for accurate tracking
-- Falls back to manual counting if events unavailable
-- [mybuffcapped] - Player's buff bar is at capacity (32 buffs)
-- Usage: [mybuffcapped] to prevent buff waste
mybuffcapped = function(conditionals)
return CleveRoids.IsPlayerBuffCapped()
end,
-- [nomybuffcapped] - Player's buff bar has room
nomybuffcapped = function(conditionals)
return not CleveRoids.IsPlayerBuffCapped()
end,
-- [mydebuffcapped] - Player's debuff bar is at capacity (16 debuffs)
mydebuffcapped = function(conditionals)
return CleveRoids.IsPlayerDebuffCapped()
end,
-- [nomydebuffcapped] - Player's debuff bar has room
nomydebuffcapped = function(conditionals)
return not CleveRoids.IsPlayerDebuffCapped()
end,
-- [debuffcapped] - Target's debuff bar is at capacity
-- For NPCs: 16 debuff slots (+ 32 overflow into buff slots = 48 visual total)
-- Usage: [debuffcapped] to stop DoT spam when target is capped
debuffcapped = function(conditionals)
local target = conditionals.target or "target"
return CleveRoids.IsTargetDebuffCapped(target)
end,
-- [nodebuffcapped] - Target's debuff bar has room
nodebuffcapped = function(conditionals)
local target = conditionals.target or "target"
return not CleveRoids.IsTargetDebuffCapped(target)
end,
-- [buffcapped] - Target's buff bar is at capacity (32 buffs)
buffcapped = function(conditionals)
local target = conditionals.target or "target"
return CleveRoids.IsTargetBuffCapped(target)
end,
-- [nobuffcapped] - Target's buff bar has room
nobuffcapped = function(conditionals)
local target = conditionals.target or "target"
return not CleveRoids.IsTargetBuffCapped(target)
end
}
+84
View File
@@ -6426,3 +6426,87 @@ SlashCmdList.EQUIPQUEUESTATUS = function()
end
end
end
-- Apply temporary weapon enchants (poisons, oils, sharpening stones, etc.)
-- Usage: /applymain [conditionals] ItemName
-- Usage: /applyoff [conditionals] ItemName
-- Example: /applymain [nomhimbue] Instant Poison
-- Example: /applyoff [combat] Crippling Poison
local function FindItemInBags(itemName)
-- Require a non-empty item name to prevent matching everything
if not itemName or itemName == "" then
return nil
end
local searchName = string.lower(itemName)
for bag = 0, 4 do
for slot = 1, GetContainerNumSlots(bag) do
local link = GetContainerItemLink(bag, slot)
if link then
local _, _, foundName = string.find(link, "%[(.+)%]")
if foundName and string.find(string.lower(foundName), searchName, 1, true) then
return bag, slot, foundName
end
end
end
end
return nil
end
function CleveRoids.DoApply(hand, msg)
local weaponSlots = {
["main"] = 16,
["off"] = 17,
}
local slot = weaponSlots[hand]
if not slot then
CleveRoids.Print("Invalid hand: " .. tostring(hand))
return false
end
local action = function(itemName)
local bag, bagSlot, foundName = FindItemInBags(itemName)
if not bag then
CleveRoids.Print("Item not found: " .. itemName)
return false
end
-- Apply the item to the weapon
UseContainerItem(bag, bagSlot)
PickupInventoryItem(slot)
ReplaceEnchant()
ClearCursor()
return true
end
local handled = false
for _, v in pairs(CleveRoids.splitStringIgnoringQuotes(msg)) do
if CleveRoids.DoWithConditionals(v, action, CleveRoids.FixEmptyTarget, false, action) then
handled = true
break
end
end
return handled
end
SLASH_APPLYMAIN1 = "/applymain"
SlashCmdList.APPLYMAIN = function(msg)
-- Require an item name argument
if not msg or msg == "" or string.gsub(msg, "%s+", "") == "" then
CleveRoids.Print("Usage: /applymain [conditionals] ItemName")
return
end
CleveRoids.DoApply("main", msg)
end
SLASH_APPLYOFF1 = "/applyoff"
SlashCmdList.APPLYOFF = function(msg)
-- Require an item name argument
if not msg or msg == "" or string.gsub(msg, "%s+", "") == "" then
CleveRoids.Print("Usage: /applyoff [conditionals] ItemName")
return
end
CleveRoids.DoApply("off", msg)
end
+86
View File
@@ -0,0 +1,86 @@
--[[
Cursive mouseover support.
Hooks into Cursive's bar OnEnter/OnLeave to track mouseover unit by GUID.
Enables [@mouseover] macros when hovering over Cursive DoT timer bars.
]]
local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
CleveRoids.Hooks = CleveRoids.Hooks or {}
local Extension = CleveRoids.RegisterExtension("CursiveMouseover")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
local hooked = false
function Extension.OnEnter(guid)
if guid and guid ~= 0 then
-- SuperWoW's SetMouseoverUnit supports GUIDs directly
CleveRoids.SetMouseoverFrom("cursive", guid)
end
end
function Extension.OnLeave()
CleveRoids.ClearMouseoverFrom("cursive")
end
local function HookCursiveUI()
-- Check if Cursive and its UI module exist
if not Cursive or not Cursive.ui then
return false
end
-- Already hooked
if hooked then
return true
end
local ui = Cursive.ui
-- Hook BarEnter if it exists
if ui.BarEnter then
CleveRoids.Hooks.CursiveBarEnter = ui.BarEnter
ui.BarEnter = function()
-- Call original
CleveRoids.Hooks.CursiveBarEnter()
-- Set mouseover from the bar's parent guid
if this and this.parent and this.parent.guid then
Extension.OnEnter(this.parent.guid)
end
end
end
-- Hook BarLeave if it exists
if ui.BarLeave then
CleveRoids.Hooks.CursiveBarLeave = ui.BarLeave
ui.BarLeave = function()
-- Clear mouseover first
Extension.OnLeave()
-- Call original
CleveRoids.Hooks.CursiveBarLeave()
end
end
hooked = true
return true
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
end
-- Also try to hook immediately in case Cursive is already loaded
if Cursive and Cursive.ui then
HookCursiveUI()
end
_G["CleveRoids"] = CleveRoids
+59 -3
View File
@@ -39,11 +39,16 @@
- GetSpellIdCooldown/GetItemIdCooldown now include item metadata:
itemId, itemHasActiveSpell, itemActiveSpellId
Auto-Attack Events (v2.24+):
- AUTO_ATTACK_SELF / AUTO_ATTACK_OTHER events
- Detailed hit info: damage, crit/glancing/crushing, dodge/parry/block
- Requires NP_EnableAutoAttackEvents=1 CVar to enable
Settings Integration:
- Reads from NampowerSettings addon when available
- Falls back to CVars when addon not present
Current version: v2.23.3
Current version: v2.24.0
]]
local _G = _G or getfenv(0)
@@ -149,12 +154,13 @@ API.VERSION_REQUIREMENTS = {
["GetItemIdCooldown"] = { 2, 18, 0, "GetItemIdCooldown" },
["ChannelStopCastingNextTick"]={ 2, 18, 0, "ChannelStopCastingNextTick" },
-- v2.20+ - Trinket API, item usage, copy parameter support
-- v2.20+ - Trinket API, item usage, copy parameter support, aura cast events
["GetTrinkets"] = { 2, 20, 0, "GetTrinkets" },
["GetTrinketCooldown"] = { 2, 20, 0, "GetTrinketCooldown" },
["UseTrinket"] = { 2, 20, 0, "UseTrinket" },
["UseItemIdOrName"] = { 2, 20, 0, "UseItemIdOrName" },
["CopyParameter"] = { 2, 20, 0 }, -- Table copy support
["AuraCastEvents"] = { 2, 20, 0 }, -- AURA_CAST_ON_SELF/OTHER events
-- v2.22+ - Utility functions
["DisenchantAll"] = { 2, 22, 0, "DisenchantAll" },
@@ -162,6 +168,9 @@ API.VERSION_REQUIREMENTS = {
-- v2.23+ - Enhanced bag queries, cooldown item metadata
["GetBagItemsWithIndex"] = { 2, 23, 0 }, -- GetBagItems(bagIndex) parameter
["CooldownItemMetadata"] = { 2, 23, 0 }, -- itemId, itemHasActiveSpell, itemActiveSpellId
-- v2.24+ - Auto-attack events
["AutoAttackEvents"] = { 2, 24, 0 }, -- AUTO_ATTACK_SELF/OTHER events
}
-- Check if a specific feature is available
@@ -250,12 +259,13 @@ local function InitializeFeatures()
f.hasGetItemIdCooldown = API.HasFeature("GetItemIdCooldown")
f.hasChannelStopCastingNextTick = API.HasFeature("ChannelStopCastingNextTick")
-- v2.20+ Trinket/Item API
-- v2.20+ Trinket/Item API and Aura Cast Events
f.hasGetTrinkets = API.HasFeature("GetTrinkets")
f.hasGetTrinketCooldown = API.HasFeature("GetTrinketCooldown")
f.hasUseTrinket = API.HasFeature("UseTrinket")
f.hasUseItemIdOrName = API.HasFeature("UseItemIdOrName")
f.hasCopyParameter = API.HasFeature("CopyParameter")
f.hasAuraCastEvents = API.HasFeature("AuraCastEvents")
-- v2.22+ Utility functions
f.hasDisenchantAll = API.HasFeature("DisenchantAll")
@@ -264,6 +274,9 @@ local function InitializeFeatures()
f.hasGetBagItemsWithIndex = API.HasFeature("GetBagItemsWithIndex")
f.hasCooldownItemMetadata = API.HasFeature("CooldownItemMetadata")
-- v2.24+ Auto-attack events
f.hasAutoAttackEvents = API.HasFeature("AutoAttackEvents")
-- Runtime detection for enhanced spell functions (verify by testing)
if f.hasEnhancedSpellFunctions and GetSpellTexture then
local success, result = pcall(function()
@@ -321,6 +334,8 @@ API.defaultSettings = {
-- v2.20+ CVars
NP_PreventMountingWhenBuffCapped = "1", -- Prevent mounting when buff capped (32 buffs)
NP_EnableAuraCastEvents = "0", -- Enable AURA_CAST_ON_SELF/OTHER events
-- v2.24+ CVars
NP_EnableAutoAttackEvents = "0", -- Enable AUTO_ATTACK_SELF/OTHER events
}
-- Get a Nampower setting value
@@ -1866,6 +1881,47 @@ API.AURA_CAP_STATUS = {
BOTH_FULL = 3,
}
-- Auto-attack event names (v2.24+, requires NP_EnableAutoAttackEvents=1)
-- Parameters: attackerGuid, targetGuid, totalDamage, hitInfo, victimState,
-- subDamageCount, blockedAmount, totalAbsorb, totalResist
API.AUTO_ATTACK_EVENTS = {
"AUTO_ATTACK_SELF", -- Fires when active player is the target
"AUTO_ATTACK_OTHER", -- Fires when active player is attacker or different unit is target
}
-- HitInfo bitfield values (for AUTO_ATTACK events)
-- Converted to decimal for Lua 5.0 compatibility (no hex literals)
API.HITINFO = {
NORMALSWING = 0,
UNK0 = 1,
AFFECTS_VICTIM = 2,
LEFTSWING = 4, -- Off-hand attack
UNK3 = 8,
MISS = 16, -- 0x10
ABSORB = 32, -- 0x20
RESIST = 64, -- 0x40
CRITICALHIT = 128, -- 0x80
UNK8 = 256, -- 0x100
UNK9 = 8192, -- 0x2000
GLANCING = 16384, -- 0x4000
CRUSHING = 32768, -- 0x8000
NOACTION = 65536, -- 0x10000
SWINGNOHITSOUND = 524288, -- 0x80000
}
-- VictimState values (for AUTO_ATTACK events)
API.VICTIMSTATE = {
UNAFFECTED = 0, -- Seen with HITINFO_MISS
NORMAL = 1,
DODGE = 2,
PARRY = 3,
INTERRUPT = 4,
BLOCKS = 5,
EVADES = 6,
IS_IMMUNE = 7,
DEFLECTS = 8,
}
-- Unit events (v2.20+)
API.UNIT_EVENTS = {
"UNIT_DIED", -- Parameters: guid
+29 -9
View File
@@ -139,6 +139,12 @@ All conditionals support negation with `no` prefix (e.g., `[nocombat]`, `[nobuff
| `notarget` | `[notarget]` | Player has no target |
| `pet` | `[pet]` `[pet:Cat/Wolf]` | Has pet (with family) |
| `name` | `[name:Onyxia]` | Exact name match (case-insensitive) |
| `tag` | `[tag]` | Target is tapped (by anyone) |
| `notag` | `[notag]` | Target is not tapped |
| `mytag` | `[mytag]` | Target is tapped by you |
| `nomytag` | `[nomytag]` | Target is not tapped by you |
| `othertag` | `[othertag]` | Target is tapped by someone else |
| `noothertag` | `[noothertag]` | Not tapped by others (yours or unclaimed) |
### Range & Position
| Conditional | Example | Description |
@@ -154,8 +160,20 @@ All conditionals support negation with `no` prefix (e.g., `[nocombat]`, `[nobuff
| Conditional | Example | Description |
|-------------|---------|-------------|
| `equipped` | `[equipped:Daggers]` | Item/type equipped |
| `mhimbue` | `[mhimbue:Flametongue]` | Main-hand weapon imbue |
| `ohimbue` | `[ohimbue:Frostbrand]` | Off-hand weapon imbue |
| `mhimbue` | `[mhimbue]` `[mhimbue:Instant_Poison]` | Main-hand has temporary imbue |
| `ohimbue` | `[ohimbue]` `[ohimbue:Crippling_Poison]` | Off-hand has temporary imbue |
**Imbue Conditionals (Poisons, Oils, Sharpening Stones):**
```lua
[mhimbue] -- Has any temporary imbue
[mhimbue:Instant_Poison] -- Has specific imbue (tooltip match)
[mhimbue:<300] -- Imbue expires in < 300 seconds (5 min)
[mhimbue:Instant_Poison<300] -- Specific imbue with time check
[mhimbue:>#5] -- Has > 5 charges remaining
[nomhimbue] -- No temporary imbue (apply needed)
```
**Note:** Temporary imbues are detected via tooltip time/charge markers, filtering out permanent enchants like Crusader.
### CC & Immunity
| Conditional | Example | Description |
@@ -255,6 +273,8 @@ These commands accept `[conditionals]` and use UnitXP 3D enemy scanning when app
| `/equipoh [cond] Item` | ✅ | — | Equip to off hand |
| `/equip11` - `/equip14 [cond]` | ✅ | — | Equip to slot (rings/trinkets) |
| `/unshift [cond]` | ✅ | — | Cancel shapeshift if conditions met |
| `/applymain [cond] Item` | ✅ | — | Apply poison/oil to main hand |
| `/applyoff [cond] Item` | ✅ | — | Apply poison/oil to off hand |
### Commands without Conditional Support
@@ -276,11 +296,11 @@ These commands accept `[conditionals]` and use UnitXP 3D enemy scanning when app
-- Kara 40 Mage Incantagos example --
/target [name:Red_Affinity]
/cast [name:Red_Affinity]Fireball
/cast [name:Red_Affinity] Fireball
/target [name:Blue_Affinity]
/cast [name:Blue_Affinity]Frostbolt
/cast [name:Blue_Affinity] Frostbolt
/target [name:Mana_Affinity]
/cast [name:Mana_Affinity]Arcane Missiles
/cast [name:Mana_Affinity] Arcane Missiles
```
If no matching target is found, your original target is preserved.
@@ -292,9 +312,9 @@ If no matching target is found, your original target is preserved.
### Debuff Timer System
Auto-learns debuff durations from your casts. 335+ spells pre-configured.
```lua
/cast [nodebuff:Moonfire] Moonfire
/cast [debuff:Moonfire<4] Moonfire
/cast Wrath
/cast [nodebuff:Moonfire] Moonfire -- Apply if missing
/cast [debuff:Moonfire<4] Moonfire -- Refresh if < 4 sec left
/cast Wrath -- Filler
```
### Combo Point Tracking
@@ -401,7 +421,7 @@ The addon checks debuffs on the target:
```lua
-- Basic: Only cast if target doesn't have Moonfire
/cast [nodebuff] Moonfire
/cast [nodebuff:Moonfire] Moonfire
-- Time check: Refresh when < 4 seconds left
/cast [debuff:Moonfire<4] Moonfire
+1
View File
@@ -36,6 +36,7 @@ Extensions\Mouseover\XPerl.lua
Extensions\Mouseover\LunaUnitFrames.lua
Extensions\Mouseover\DragonflightReloaded.lua
Extensions\Mouseover\Dragonflight3.lua
Extensions\Mouseover\Cursive.lua
Extensions\Tooltip\Generic.lua
Extensions\CursiveCustomSpells.lua
+1
View File
@@ -254,6 +254,7 @@ do
aguf = 3, -- ag_UnitFrames
ctra = 3, -- CT_RaidAssist
ctuf = 3, -- CT_UnitFrames
cursive = 3, -- Cursive DoT tracker bars
duf = 3, -- DiscordUnitFrames
focus = 3, -- FocusFrame
grid = 3, -- Grid