clarify actual multi and noable multi usage, add AND operator for multi, and fix swintimer to be more intuitive.

This commit is contained in:
Jrc13245
2025-11-25 14:23:54 -05:00
parent 43ef919d79
commit a33f3f21db
4 changed files with 197 additions and 91 deletions
+88 -59
View File
@@ -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
}
+27 -2
View File
@@ -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, '"', "")
+1
View File
@@ -107,6 +107,7 @@ CleveRoids.ignoreKeywords = {
action = true,
ignoretooltip = true,
cancelaura = true,
_operators = true, -- Metadata for AND/OR operator tracking
}
-- TODO: Localize?
+81 -30
View File
@@ -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 #.<br/>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]<br/>[usable:"Spell Name"] | * | * | If the spell or item is usable (not on cooldown, have reagents, etc.). |
| zone | [zone:"Zone"]<br/>[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
- `[swingtimer:<=X]` - True if swing timer <= X% of attack speed
- `[stimer:>X]` - Alias for swingtimer
- `[swingtimer:<X]` - True if less than X% of swing has elapsed (early in swing)
- `[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:<X]` - Alias for swingtimer
**Negation:**
- `[noswingtimer:<80]` - True if NOT (swing timer < 80% of attack speed)
- `[nostimer:>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)
---