From 7d0dec2a2bb782072afaa948b7007e5c6cd3d391 Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Mon, 12 Jan 2026 17:13:40 -0500 Subject: [PATCH] cursive mouseover support, applymain and applyoff slashcommands, nampower feature update with incominghit, lastswing, buffcap, debuffcap conditionals --- Conditionals.lua | 750 ++++++++++++++++++++++++++++--- Core.lua | 84 ++++ Extensions/Mouseover/Cursive.lua | 86 ++++ NampowerAPI.lua | 62 ++- README.md | 38 +- SuperCleveRoidMacros.toc | 1 + Utility.lua | 1 + 7 files changed, 943 insertions(+), 79 deletions(-) create mode 100644 Extensions/Mouseover/Cursive.lua diff --git a/Conditionals.lua b/Conditionals.lua index 70f5fc2..7a26b87 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -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 } diff --git a/Core.lua b/Core.lua index 8705cc9..ba63a08 100644 --- a/Core.lua +++ b/Core.lua @@ -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 diff --git a/Extensions/Mouseover/Cursive.lua b/Extensions/Mouseover/Cursive.lua new file mode 100644 index 0000000..335015b --- /dev/null +++ b/Extensions/Mouseover/Cursive.lua @@ -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 diff --git a/NampowerAPI.lua b/NampowerAPI.lua index b17a326..18c500f 100644 --- a/NampowerAPI.lua +++ b/NampowerAPI.lua @@ -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 diff --git a/README.md b/README.md index 562e7ae..ec23b29 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/SuperCleveRoidMacros.toc b/SuperCleveRoidMacros.toc index cdd2f19..81a5e40 100644 --- a/SuperCleveRoidMacros.toc +++ b/SuperCleveRoidMacros.toc @@ -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 diff --git a/Utility.lua b/Utility.lua index 5e352aa..8e1d0a3 100644 --- a/Utility.lua +++ b/Utility.lua @@ -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