From a33f3f21db98bcb9a4bb59aa405b721dab6f9c67 Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Tue, 25 Nov 2025 14:23:54 -0500 Subject: [PATCH] clarify actual multi and noable multi usage, add AND operator for multi, and fix swintimer to be more intuitive. --- Conditionals.lua | 147 ++++++++++++++++++++++++++++------------------- Core.lua | 29 +++++++++- Init.lua | 1 + README.md | 111 +++++++++++++++++++++++++---------- 4 files changed, 197 insertions(+), 91 deletions(-) diff --git a/Conditionals.lua b/Conditionals.lua index 7340c20..8f716b4 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -75,6 +75,31 @@ local function Or(t,func) return false end +-- Helper to choose And() or Or() based on operator metadata +-- For negated conditionals: +-- - If operator is OR (or not specified), use Or() -> any negation matches +-- - If operator is AND, use And() -> all negations must match +local function NegatedMulti(t, func, conditionals, condition) + if type(func) ~= "function" then return false end + if type(t) ~= "table" then + t = { [1] = t } + end + + -- Check operator type from metadata + local operatorType = "OR" -- default + if conditionals and conditionals._operators and conditionals._operators[condition] then + operatorType = conditionals._operators[condition] + end + + if operatorType == "AND" then + -- AND operator: ALL values must fail the check (original And behavior) + return And(t, func) + else + -- OR operator (default): ANY value can fail the check + return Or(t, func) + end +end + -- pfUI debuff time helper (Vanilla 1.12.1 / Lua 5.0 safe) local function PFUI_HasLibDebuff() return type(pfUI) == "table" @@ -340,8 +365,8 @@ end -- Validates swing timer percentage for SP_SwingTimer addon integration -- operator: Comparison operator (>, <, =, >=, <=, ~=) --- amount: Percentage of swing time (e.g., 20 means 20% of attack speed) --- returns: True if st_timer [operator] (attackSpeed * percent/100) +-- amount: Percentage of swing time elapsed (e.g., 20 means 20% of swing has elapsed) +-- returns: True if percentElapsed [operator] amount function CleveRoids.ValidateSwingTimer(operator, amount) if not operator or not amount then return false end @@ -359,12 +384,15 @@ function CleveRoids.ValidateSwingTimer(operator, amount) local attackSpeed = UnitAttackSpeed("player") if not attackSpeed or attackSpeed <= 0 then return false end - -- Calculate threshold: percentage of swing time - local threshold = attackSpeed * (amount / 100) + -- Calculate percentage of swing elapsed + -- st_timer counts down from attackSpeed to 0 (time remaining) + -- So: timeElapsed = attackSpeed - st_timer + local timeElapsed = attackSpeed - st_timer + local percentElapsed = (timeElapsed / attackSpeed) * 100 - -- Compare current swing timer against threshold + -- Compare percent elapsed against threshold if CleveRoids.operators[operator] then - return CleveRoids.comparators[operator](st_timer, threshold) + return CleveRoids.comparators[operator](percentElapsed, amount) end return false @@ -1276,9 +1304,9 @@ CleveRoids.Keywords = { if type(forbiddenStances) ~= "table" then return i == 0 end - return And(forbiddenStances, function (v) + return NegatedMulti(forbiddenStances, function (v) return (i ~= tonumber(v)) - end) + end, conditionals, "nostance") end, noform = function(conditionals) @@ -1287,9 +1315,9 @@ CleveRoids.Keywords = { if type(forbiddenForms) ~= "table" then return i == 0 end - return And(forbiddenForms, function (v) + return NegatedMulti(forbiddenForms, function (v) return (i ~= tonumber(v)) - end) + end, conditionals, "noform") end, form = function(conditionals) @@ -1312,9 +1340,9 @@ CleveRoids.Keywords = { if type(conditionals.nomod) ~= "table" then return CleveRoids.kmods.nomod() end - return And(conditionals.nomod, function(mod) + return NegatedMulti(conditionals.nomod, function(mod) return not CleveRoids.kmods[mod]() - end) + end, conditionals, "nomod") end, target = function(conditionals) @@ -1339,12 +1367,12 @@ CleveRoids.Keywords = { -- Check if an argument like :target or :focus was provided. if type(conditionals.nocombat) == "table" then -- If so, run the check on the provided unit(s). - return And(conditionals.nocombat, function(unit) + return NegatedMulti(conditionals.nocombat, function(unit) if not UnitExists(unit) then return true end return not UnitAffectingCombat(unit) - end) + end, conditionals, "nocombat") else -- Otherwise, this is a bare [nocombat]. Default to checking the player. return not UnitAffectingCombat("player") @@ -1374,9 +1402,9 @@ CleveRoids.Keywords = { nocasting = function(conditionals) if type(conditionals.nocasting) ~= "table" then return not CleveRoids.CheckSpellCast(conditionals.target, "") end - return And(conditionals.nocasting, function (spell) + return NegatedMulti(conditionals.nocasting, function (spell) return not CleveRoids.CheckSpellCast(conditionals.target, spell) - end) + end, conditionals, "nocasting") end, -- NEW: Direct player casting check using Nampower's GetCurrentCastingInfo @@ -1415,9 +1443,9 @@ CleveRoids.Keywords = { nozone = function(conditionals) local zone = GetRealZoneText() local sub_zone = GetSubZoneText() - return And(conditionals.nozone, function (v) + return NegatedMulti(conditionals.nozone, function (v) return not ((sub_zone ~= "" and v == sub_zone)) or (v == zone) - end) + end, conditionals, "nozone") end, equipped = function(conditionals) @@ -1491,9 +1519,9 @@ CleveRoids.Keywords = { end, noreactive = function(conditionals) - return And(conditionals.noreactive, function (v) + return NegatedMulti(conditionals.noreactive, function (v) return not CleveRoids.IsReactiveUsable(v) - end) + end, conditionals, "noreactive") end, usable = function(conditionals) @@ -1530,7 +1558,7 @@ CleveRoids.Keywords = { end, nousable = function(conditionals) - return And(conditionals.nousable, function(name) + return NegatedMulti(conditionals.nousable, function(name) -- If checking a reactive spell, use reactive logic if CleveRoids.reactiveSpells[name] then return not CleveRoids.IsReactiveUsable(name) @@ -1559,7 +1587,7 @@ CleveRoids.Keywords = { -- Check item cooldown (>0 remaining = not usable) local remaining = CleveRoids.GetItemCooldown(itemName) return remaining > 0 - end) + end, conditionals, "nousable") end, member = function(conditionals) @@ -1612,9 +1640,9 @@ CleveRoids.Keywords = { end, nobuff = function(conditionals) - return And(conditionals.nobuff, function(v) + return NegatedMulti(conditionals.nobuff, function(v) return not CleveRoids.ValidateUnitBuff(conditionals.target, v) - end) + end, conditionals, "nobuff") end, debuff = function(conditionals) @@ -1624,9 +1652,9 @@ CleveRoids.Keywords = { end, nodebuff = function(conditionals) - return And(conditionals.nodebuff, function(v) + return NegatedMulti(conditionals.nodebuff, function(v) return not CleveRoids.ValidateUnitDebuff(conditionals.target, v) - end) + end, conditionals, "nodebuff") end, mybuff = function(conditionals) @@ -1636,9 +1664,9 @@ CleveRoids.Keywords = { end, nomybuff = function(conditionals) - return And(conditionals.nomybuff, function(v) + return NegatedMulti(conditionals.nomybuff, function(v) return not CleveRoids.ValidatePlayerBuff(v) - end) + end, conditionals, "nomybuff") end, mydebuff = function(conditionals) @@ -1648,9 +1676,9 @@ CleveRoids.Keywords = { end, nomydebuff = function(conditionals) - return And(conditionals.nomydebuff, function(v) + return NegatedMulti(conditionals.nomydebuff, function(v) return not CleveRoids.ValidatePlayerDebuff(v) - end) + end, conditionals, "nomydebuff") end, power = function(conditionals) @@ -1765,9 +1793,9 @@ CleveRoids.Keywords = { end, notype = function(conditionals) - return And(conditionals.notype, function(unittype) + return NegatedMulti(conditionals.notype, function(unittype) return not CleveRoids.ValidateCreatureType(unittype, conditionals.target) - end) + end, conditionals, "notype") end, cooldown = function(conditionals) @@ -1777,9 +1805,9 @@ CleveRoids.Keywords = { end, nocooldown = function(conditionals) - return And(conditionals.nocooldown,function (v) + return NegatedMulti(conditionals.nocooldown,function (v) return not CleveRoids.ValidateCooldown(v, true) - end) + end, conditionals, "nocooldown") end, cdgcd = function(conditionals) @@ -1789,9 +1817,9 @@ CleveRoids.Keywords = { end, nocdgcd = function(conditionals) - return And(conditionals.nocdgcd,function (v) + return NegatedMulti(conditionals.nocdgcd,function (v) return not CleveRoids.ValidateCooldown(v, false) - end) + end, conditionals, "nocdgcd") end, channeled = function(conditionals) @@ -1817,9 +1845,9 @@ CleveRoids.Keywords = { end, notargeting = function(conditionals) - return And(conditionals.notargeting, function (unit) + return NegatedMulti(conditionals.notargeting, function (unit) return UnitIsUnit("targettarget", unit) ~= 1 - end) + end, conditionals, "notargeting") end, isplayer = function(conditionals) @@ -1850,7 +1878,7 @@ CleveRoids.Keywords = { noinrange = function(conditionals) if not IsSpellInRange then return end - return And(conditionals.noinrange, function(spellName) + return NegatedMulti(conditionals.noinrange, function(spellName) local target = conditionals.target or "target" local checkValue = spellName or conditionals.action @@ -1862,7 +1890,7 @@ CleveRoids.Keywords = { end return IsSpellInRange(checkValue, target) == 0 - end) + end, conditionals, "noinrange") end, outrange = function(conditionals) @@ -1889,9 +1917,9 @@ CleveRoids.Keywords = { end, nocombo = function(conditionals) - return And(conditionals.nocombo, function(args) + return NegatedMulti(conditionals.nocombo, function(args) return not CleveRoids.ValidateComboPoints(args.operator, args.amount) - end) + end, conditionals, "nocombo") end, known = function(conditionals) @@ -1901,9 +1929,9 @@ CleveRoids.Keywords = { end, noknown = function(conditionals) - return And(conditionals.noknown, function(args) + return NegatedMulti(conditionals.noknown, function(args) return not CleveRoids.ValidateKnown(args) - end) + end, conditionals, "noknown") end, resting = function() @@ -1991,10 +2019,10 @@ CleveRoids.Keywords = { -- A player should always have a class, but if not, this condition is still met. if not localizedClass then return true end - -- The "And" helper ensures the player's class is not any of the forbidden classes. - return And(conditionals.noclass, function(forbiddenClass) + -- The "NegatedMulti" helper ensures the player's class is not any of the forbidden classes. + return NegatedMulti(conditionals.noclass, function(forbiddenClass) return string.lower(forbiddenClass) ~= string.lower(localizedClass) and string.lower(forbiddenClass) ~= string.lower(englishClass) - end) + end, conditionals, "noclass") end, pet = function(conditionals) @@ -2016,13 +2044,13 @@ CleveRoids.Keywords = { return true end - return And(conditionals.nopet, function(petType) + return NegatedMulti(conditionals.nopet, function(petType) local currentPet = UnitCreatureFamily("pet") if not currentPet then return true end return string.lower(currentPet) ~= string.lower(petType) - end) + end, conditionals, "nopet") end, swimming = function(conditionals) @@ -2056,7 +2084,7 @@ CleveRoids.Keywords = { nodistance = function(conditionals) if not CleveRoids.hasUnitXP then return false end - return And(conditionals.nodistance, function(args) + return NegatedMulti(conditionals.nodistance, function(args) if type(args) ~= "table" or not args.operator or not args.amount then return false end @@ -2068,7 +2096,7 @@ CleveRoids.Keywords = { if not distance then return false end return not CleveRoids.comparators[args.operator](distance, args.amount) - end) + end, conditionals, "nodistance") end, behind = function(conditionals) @@ -2163,11 +2191,11 @@ CleveRoids.Keywords = { return true end - return And(conditionals.noqueuedspell, function(spellName) + return NegatedMulti(conditionals.noqueuedspell, function(spellName) local queuedName = string.gsub(CleveRoids.queuedSpell.spellName, "%s*%(.-%)%s*$", "") local checkName = string.gsub(spellName, "%s*%(.-%)%s*$", "") return string.lower(queuedName) ~= string.lower(checkName) - end) + end, conditionals, "noqueuedspell") end, onswingpending = function(conditionals) @@ -2301,8 +2329,9 @@ CleveRoids.Keywords = { end, -- SP_SwingTimer integration conditionals - -- Checks if swing timer is at a percentage of attack speed - -- Usage: [swingtimer:>20] = st_timer > UnitAttackSpeed("player") * 0.2 + -- Checks percentage of swing time that has elapsed + -- Usage: [swingtimer:<15] = less than 15% of swing has elapsed (early in swing) + -- [swingtimer:>80] = more than 80% of swing has elapsed (late in swing) swingtimer = function(conditionals) return And(conditionals.swingtimer, function(args) if type(args) ~= "table" then return false end @@ -2320,17 +2349,17 @@ CleveRoids.Keywords = { -- Negated swingtimer noswingtimer = function(conditionals) - return And(conditionals.noswingtimer, function(args) + return NegatedMulti(conditionals.noswingtimer, function(args) if type(args) ~= "table" then return false end return not CleveRoids.ValidateSwingTimer(args.operator, args.amount) - end) + end, conditionals, "noswingtimer") end, -- Alias for noswingtimer nostimer = function(conditionals) - return And(conditionals.nostimer, function(args) + return NegatedMulti(conditionals.nostimer, function(args) if type(args) ~= "table" then return false end return not CleveRoids.ValidateSwingTimer(args.operator, args.amount) - end) + end, conditionals, "nostimer") end } diff --git a/Core.lua b/Core.lua index 8c28906..a1edf47 100644 --- a/Core.lua +++ b/Core.lua @@ -1239,8 +1239,33 @@ function CleveRoids.ParseMsg(msg) conditionals[condition] = { conditionals[condition] } end - -- Split args by '/' for multiple values - for _, arg_item in CleveRoids.splitString(args, "/") do + -- Detect which separator is used: / (OR) or & (AND) + -- Initialize metadata table if needed + if not conditionals._operators then + conditionals._operators = {} + end + + -- Check which separator is present + local hasSlash = string.find(args, "/") + local hasAmpersand = string.find(args, "&") + local separator = "/" + local operatorType = "OR" + + if hasAmpersand and not hasSlash then + separator = "&" + operatorType = "AND" + elseif hasAmpersand and hasSlash then + -- Both separators present - default to / (OR) and warn + -- Could add a warning here in the future + separator = "/" + operatorType = "OR" + end + + -- Store the operator type for this conditional + conditionals._operators[condition] = operatorType + + -- Split args by the determined separator + for _, arg_item in CleveRoids.splitString(args, separator) do local processed_arg = CleveRoids.Trim(arg_item) processed_arg = string.gsub(processed_arg, '"', "") diff --git a/Init.lua b/Init.lua index caccea2..60a5bfc 100644 --- a/Init.lua +++ b/Init.lua @@ -107,6 +107,7 @@ CleveRoids.ignoreKeywords = { action = true, ignoretooltip = true, cancelaura = true, + _operators = true, -- Metadata for AND/OR operator tracking } -- TODO: Localize? diff --git a/README.md b/README.md index 009c335..2944bbe 100644 --- a/README.md +++ b/README.md @@ -93,27 +93,41 @@ Both single-line and multi-line formats work identically: * **Default**: Most conditionals default to `@target` if not specified ### Multi-Value Conditionals -Some conditionals accept multiple values with `/` separator: +Some conditionals accept multiple values with operators: + +#### OR Operator (`/`) * **Any match wins**: `[zone:Stormwind/Ironforge]` = in Stormwind OR Ironforge +* **Works with negation**: `[nozone:Stormwind_City/Ironforge]` = NOT in Stormwind OR NOT in Ironforge + * True if you're outside either zone (or both) + * Example: `[nomypower:>10/<20]` = power is not >10 OR not <20 (true if power is 5-10 or ≥20) + +#### AND Operator (`&`) +* **All must match**: `[zone:Stormwind_City&Elwynn_Forest]` = in both zones (subzone & zone check) +* **Works with negation**: `[nozone:Stormwind_City&Ironforge]` = NOT in Stormwind AND NOT in Ironforge + * True only if you're in neither zone + * Example: `[nomybuff:Mark_of_the_Wild&Thorns]` = missing both Mark AND Thorns + +**Important**: The `/` operator ALWAYS means OR, even for negated conditionals. Use `&` for AND logic. + * **Marked as "Multi"** in the conditionals table below ### Negation (No-able Conditionals) Prefix with `no` to negate: -* **All must be false**: `[nozone:Stormwind/Ironforge]` = NOT in Stormwind AND NOT in Ironforge * **Marked as "Noable"** in the conditionals table below +* **Operators apply**: Use `/` for OR logic, `&` for AND logic (see above) ### Numerical Comparisons For hp, power, cooldown, etc: * **Operators**: `<`, `>`, `=`, `<=`, `>=`, `~=` * **Format**: `[condition:>50]` or `[condition:"Name">50]` * **Stacks vs Time**: Use `>#` for stacks/rank, no `#` for time - * `[buff:"Mark of the Wild">#5]` = 5+ stacks + * `[buff:"Mark_of_the_Wild">#5]` = 5+ stacks * `[mybuff:"Renew"<4]` = less than 4 seconds remaining ### Omitting Values If the conditional value matches the action, you can omit it: -* `[debuff:"Sunder Armor"<#5] Sunder Armor` = `[debuff:<#5] Sunder Armor` -* `[nobuff:"Mark of the Wild"] Mark of the Wild` = `[nobuff] Mark of the Wild` +* `[debuff:"Sunder_Armor"<#5] Sunder Armor` = `[debuff:<#5] Sunder Armor` +* `[nobuff:"Mark_of_the_Wild"] Mark of the Wild` = `[nobuff] Mark of the Wild` ### Item IDs Use item IDs instead of names to avoid cache issues: @@ -162,6 +176,41 @@ Use slot numbers (1-19) with `/use`: /cast Faerie Fire (Feral)(Rank 4) -- CORRECT ``` +### Multi-Value Operator Examples + +**OR Operator (`/`)** - Any value matches: +``` +# Cast if in Stormwind OR Ironforge +/cast [zone:Stormwind_City/Ironforge] Hearthstone + +# Cast if NOT in Stormwind OR NOT in Ironforge (true if outside either/both) +/cast [nozone:Stormwind_City/Ironforge] Mount + +# Cast if power is NOT >10 OR NOT <20 (true if power is 5-10 or ≥20) +/cast [nomypower:>10/<20] Spell +``` + +**AND Operator (`&`)** - All values must match: +``` +# Cast if missing both Mark of the Wild AND Thorns +/cast [nobuff:Mark_of_the_Wild&Thorns] Mark of the Wild + +# Cast if NOT alt AND NOT ctrl (only true if pressing neither) +/cast [nomod:alt&ctrl] Fireball + +# Cast if target has neither Corruption AND Curse of Agony +/cast [nodebuff:Corruption&Curse_of_Agony] Corruption +``` + +**Practical Comparison**: +``` +# OR: Reapply if missing EITHER buff +/cast [nomybuff:Mark_of_the_Wild/Thorns] Mark of the Wild + +# AND: Only apply if missing BOTH buffs +/cast [nomybuff:Mark_of_the_Wild&Thorns] Mark of the Wild +``` + --- # What's Different @@ -572,8 +621,8 @@ See the **[Macro Syntax Guide](#macro-syntax-guide)** above for detailed syntax | stance | [stance:0/1/2/3/4/5] | * | * | If the player is in stance #.
Supports Shadowform and Stealth as stance 1.| | stat | [stat:stat>=x/<=y] | * | | Check if one of the players statistics is greater or less than a specific number. Available Stats: str/strength, agi/agility, stam/stamina, int/intellect, spi/spirit, ap/attackpower, rap/rangedattackpower, healing/healingpower, arcane_power, fire_power, frost_power, nature_power, shadow_power, armor, defense, arcane_res, fire_res, frost_res, nature_res, shadow_res. | | stealth | [stealth] | | * | If the player is in Stealth or Prowl. | -| swingtimer | [swingtimer:>20] | * | * | If the swing timer is at a percentage of attack speed. Requires [SP_SwingTimer](https://github.com/jrc13245/SP_SwingTimer) addon. See [below](#swing-timer-integration) for details.| -| stimer | [stimer:<=50] | * | * | Alias for `swingtimer` | +| swingtimer | [swingtimer:<15] | * | * | If a percentage of swing time has elapsed. `<15` = early in swing, `>80` = late in swing. Requires [SP_SwingTimer](https://github.com/jrc13245/SP_SwingTimer) addon. See [below](#swing-timer-integration) for details.| +| stimer | [stimer:<15] | * | * | Alias for `swingtimer` | | swimming | [swimming] | | * | Druid only, works like reactive but for aquatic form, must have aquatic form on one of your non-stance actionbars. | | usable | [usable]
[usable:"Spell Name"] | * | * | If the spell or item is usable (not on cooldown, have reagents, etc.). | | zone | [zone:"Zone"]
[zone:"Zone"/"Another Zone"] | * | * | If the player is in one or more zones of the given name. | @@ -733,54 +782,56 @@ The addon integrates with [SP_SwingTimer](https://github.com/jrc13245/SP_SwingTi - If SP_SwingTimer is not loaded, the conditional will return false and display an error message once **How It Works:** -The conditional compares the current swing timer (`st_timer`) against a percentage of your attack speed. +The conditional checks what percentage of your swing time has **elapsed** (not remaining). -`[swingtimer:>20]` checks if `st_timer > UnitAttackSpeed("player") * 0.2` +`[swingtimer:<15]` checks if less than 15% of your swing has elapsed (early in swing). -This means "cast if more than 20% of my swing time remains." +This allows you to cast abilities like Slam at the optimal time in your swing cycle. **Syntax:** -- `[swingtimer:>X]` - True if swing timer > X% of attack speed -- `[swingtimer:=X]` - True if swing timer >= X% of attack speed -- `[swingtimer:<=X]` - True if swing timer <= X% of attack speed -- `[stimer:>X]` - Alias for swingtimer +- `[swingtimer:X]` - True if more than X% of swing has elapsed (late in swing) +- `[swingtimer:>=X]` - True if X% or more of swing has elapsed +- `[swingtimer:<=X]` - True if X% or less of swing has elapsed +- `[stimer:20]` - Alias for noswingtimer +- `[noswingtimer:<15]` - True if NOT (less than 15% elapsed) = 15% or more has elapsed +- `[nostimer:>80]` - Alias for noswingtimer **Example Macros:** ```lua --- Cast Slam only if more than 20% of swing time remains --- Equivalent to: /run if st_timer>UnitAttackSpeed("player")*0.2 then CastSpellByName("Slam") end +-- Cast Slam early in the swing (first 15% of swing time) +-- This is optimal for Slam to avoid delaying your next auto-attack #showtooltip Slam -/cast [swingtimer:>20] Slam +/cast [swingtimer:<15] Slam ``` ```lua --- Heroic Strike queue management - only queue if swing timer is low +-- Heroic Strike queue management - only queue late in swing #showtooltip Heroic Strike -/cast [stimer:<30] Heroic Strike +/cast [stimer:>70] Heroic Strike ``` ```lua -- Complex rotation with swing timer awareness #showtooltip -/cast [swingtimer:>25] Slam -/cast Heroic Strike +/cast [swingtimer:<15] Slam +/cast [swingtimer:>80] Heroic Strike +/cast Bloodthirst ``` **Understanding the Percentage:** -- 100% = Full swing time (just attacked, full cooldown remaining) -- 50% = Half of swing time remaining -- 20% = Only 20% of swing time left before next auto-attack -- 0% = About to swing +- 0% = Just attacked (swing just started) +- 15% = Early in swing (optimal for Slam) +- 50% = Half of swing time has elapsed +- 80% = Late in swing (close to next auto-attack) +- 100% = About to swing For a warrior with a 3.5 second weapon: -- `[swingtimer:>20]` = True if st_timer > 0.7 seconds -- `[swingtimer:>50]` = True if st_timer > 1.75 seconds +- `[swingtimer:<15]` = True when less than 0.525 seconds have elapsed (early) +- `[swingtimer:>80]` = True when more than 2.8 seconds have elapsed (late) ---