diff --git a/Conditionals.lua b/Conditionals.lua index fa13310..f32fede 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -5508,6 +5508,51 @@ function CleveRoids.IsUnitInMeleeRange(unit, cleaveRange) return CheckInteractDistance(unit, 3) end +-- Count mode filter functions for compound conditionals. +-- Allows count mode to combine checks: [meleerange:facing>1] counts enemies +-- in melee range AND facing. The filter name is the "name" field in count args. +local countModeFilters = { + facing = function(unit) + return UnitXP("behind", unit, "player") ~= true + end, + behind = function(unit) + return UnitXP("behind", "player", unit) == true + end, + meleerange = function(unit) + return CleveRoids.IsUnitInMeleeRange(unit, true) + end, +} + +-- Parse the name field of count mode args for optional filter and distance threshold. +-- Supports: "facing", "behind", "meleerange" (filter only), +-- "30" (distance only), +-- "30:facing" or "30facing" (distance + filter) +-- Returns: filterFunc or nil, distanceThreshold or nil +local function ParseCountModeFilter(name) + if not name then return nil, nil end + + local lower = string.lower(name) + local filter = countModeFilters[lower] + if filter then return filter, nil end + + local dist = tonumber(name) + if dist then return nil, dist end + + -- Compound with colon: "30:facing" (from distance:30:facing>1) + local num, qualifier = string.match(name, "^(%d+%.?%d*):(%a+)$") + if num and qualifier then + return countModeFilters[string.lower(qualifier)], tonumber(num) + end + + -- Compound without separator: "30facing" + num, qualifier = string.match(name, "^(%d+%.?%d*)(%a+)$") + if num and qualifier then + return countModeFilters[string.lower(qualifier)], tonumber(num) + end + + return nil, nil +end + -- A list of Conditionals and their functions to validate them CleveRoids.Keywords = { exists = function(conditionals) @@ -7137,6 +7182,9 @@ CleveRoids.Keywords = { return PlayerIsRooted() ~= 1 end, + -- [distance:<30] - Target is within 30 yards + -- [distance:30>1] - More than 1 enemy within 30 yards (count mode) + -- [distance:30facing>1] - More than 1 enemy within 30 yards AND facing (compound count) distance = function(conditionals) if not CleveRoids.hasUnitXP then return false end @@ -7145,6 +7193,19 @@ CleveRoids.Keywords = { return false end + -- Count mode: name encodes distance threshold + optional filter + local filter, distThreshold = ParseCountModeFilter(args.name) + if distThreshold then + local count = CleveRoids.CountEnemiesMatching(function(unit) + local dist = UnitXP("distanceBetween", "player", unit) + if not dist or dist > distThreshold then return false end + if filter and not filter(unit) then return false end + return true + end) + return CleveRoids.comparators[args.operator](count, args.amount) + end + + -- Single-target: [distance:<30] local unit = conditionals.target or "target" if not UnitExists(unit) then return false end @@ -7155,6 +7216,9 @@ CleveRoids.Keywords = { end, conditionals, "distance") end, + -- [nodistance:<30] - Target is NOT within 30 yards + -- [nodistance:30>1] - More than 1 enemy NOT within 30 yards (count mode) + -- [nodistance:30facing>1] - More than 1 enemy NOT within 30 yards AND facing (compound count) nodistance = function(conditionals) if not CleveRoids.hasUnitXP then return false end @@ -7163,6 +7227,19 @@ CleveRoids.Keywords = { return false end + -- Count mode: name encodes distance threshold + optional filter + local filter, distThreshold = ParseCountModeFilter(args.name) + if distThreshold then + local count = CleveRoids.CountEnemiesMatching(function(unit) + local dist = UnitXP("distanceBetween", "player", unit) + if dist and dist <= distThreshold then return false end + if filter and not filter(unit) then return false end + return true + end) + return CleveRoids.comparators[args.operator](count, args.amount) + end + + -- Single-target: [nodistance:<30] local unit = conditionals.target or "target" if not UnitExists(unit) then return false end @@ -7175,82 +7252,105 @@ CleveRoids.Keywords = { -- [behind] - Player is behind target -- [behind:>N] - Player is behind more than N enemies (count mode) + -- [behind:meleerange>N] - Behind more than N enemies AND in melee range (compound count) + -- [behind:10>N] - Behind more than N enemies AND within 10 yards (compound count) behind = function(conditionals) if not CleveRoids.hasUnitXP then return false end - -- Check for count mode: [behind:>1] local countArgs = CleveRoids.GetCountModeArgs(conditionals.behind) if countArgs then + local filter, distThreshold = ParseCountModeFilter(countArgs.name) local count = CleveRoids.CountEnemiesMatching(function(unit) - return UnitXP("behind", "player", unit) == true + if UnitXP("behind", "player", unit) ~= true then return false end + if distThreshold then + local dist = UnitXP("distanceBetween", "player", unit) + if not dist or dist > distThreshold then return false end + end + if filter and not filter(unit) then return false end + return true end) return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) end - -- Original single-target behavior local unit = conditionals.target or "target" if not UnitExists(unit) then return false end - return UnitXP("behind", "player", unit) == true end, -- [nobehind] - Player is NOT behind target - -- [nobehind:>N] - Player is NOT behind more than N enemies (count mode) + -- [nobehind:>N] - NOT behind more than N enemies (count mode) + -- [nobehind:meleerange>N] - NOT behind more than N enemies AND in melee range (compound count) nobehind = function(conditionals) if not CleveRoids.hasUnitXP then return false end - -- Check for count mode: [nobehind:>1] local countArgs = CleveRoids.GetCountModeArgs(conditionals.nobehind) if countArgs then + local filter, distThreshold = ParseCountModeFilter(countArgs.name) local count = CleveRoids.CountEnemiesMatching(function(unit) - return UnitXP("behind", "player", unit) ~= true + if UnitXP("behind", "player", unit) == true then return false end + if distThreshold then + local dist = UnitXP("distanceBetween", "player", unit) + if not dist or dist > distThreshold then return false end + end + if filter and not filter(unit) then return false end + return true end) return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) end - -- Original single-target behavior local unit = conditionals.target or "target" if not UnitExists(unit) then return false end - return UnitXP("behind", "player", unit) ~= true end, -- [facing] - Player is facing target (target is not behind player) -- [facing:>N] - Player is facing more than N enemies (count mode) + -- [facing:meleerange>N] - Facing more than N enemies AND in melee range (compound count) + -- [facing:10>N] - Facing more than N enemies AND within 10 yards (compound count) facing = function(conditionals) if not CleveRoids.hasUnitXP then return false end - -- Check for count mode: [facing:>1] local countArgs = CleveRoids.GetCountModeArgs(conditionals.facing) if countArgs then + local filter, distThreshold = ParseCountModeFilter(countArgs.name) local count = CleveRoids.CountEnemiesMatching(function(unit) - return UnitXP("behind", unit, "player") ~= true + if UnitXP("behind", unit, "player") == true then return false end + if distThreshold then + local dist = UnitXP("distanceBetween", "player", unit) + if not dist or dist > distThreshold then return false end + end + if filter and not filter(unit) then return false end + return true end) return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) end - -- Original single-target behavior local unit = conditionals.target or "target" if not UnitExists(unit) then return false end - return UnitXP("behind", unit, "player") ~= true end, -- [nofacing] - Player is NOT facing target (target is behind player) - -- [nofacing:>N] - Player is NOT facing more than N enemies (count mode) + -- [nofacing:>N] - NOT facing more than N enemies (count mode) + -- [nofacing:meleerange>N] - NOT facing more than N enemies AND in melee range (compound count) nofacing = function(conditionals) if not CleveRoids.hasUnitXP then return false end - -- Check for count mode: [nofacing:>1] local countArgs = CleveRoids.GetCountModeArgs(conditionals.nofacing) if countArgs then + local filter, distThreshold = ParseCountModeFilter(countArgs.name) local count = CleveRoids.CountEnemiesMatching(function(unit) - return UnitXP("behind", unit, "player") == true + if UnitXP("behind", unit, "player") ~= true then return false end + if distThreshold then + local dist = UnitXP("distanceBetween", "player", unit) + if not dist or dist > distThreshold then return false end + end + if filter and not filter(unit) then return false end + return true end) return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) end - -- Original single-target behavior local unit = conditionals.target or "target" if not UnitExists(unit) then return false end @@ -7301,39 +7401,41 @@ CleveRoids.Keywords = { -- [meleerange] - Target is in melee range -- [meleerange:>N] - More than N enemies are in melee range (count mode) + -- [meleerange:facing>N] - More than N enemies in melee range AND facing (compound count) meleerange = function(conditionals) - -- Check for count mode: [meleerange:>1] local countArgs = CleveRoids.GetCountModeArgs(conditionals.meleerange) if countArgs then + local filter = ParseCountModeFilter(countArgs.name) local count = CleveRoids.CountEnemiesMatching(function(unit) - return CleveRoids.IsUnitInMeleeRange(unit, true) + if not CleveRoids.IsUnitInMeleeRange(unit, true) then return false end + if filter and not filter(unit) then return false end + return true end) return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) end - -- Original single-target behavior local unit = conditionals.target or "target" if not UnitExists(unit) then return false end - return CleveRoids.IsUnitInMeleeRange(unit) end, -- [nomeleerange] - Target is NOT in melee range -- [nomeleerange:>N] - More than N enemies are NOT in melee range (count mode) + -- [nomeleerange:facing>N] - More than N enemies NOT in melee range AND facing (compound count) nomeleerange = function(conditionals) - -- Check for count mode: [nomeleerange:>1] local countArgs = CleveRoids.GetCountModeArgs(conditionals.nomeleerange) if countArgs then + local filter = ParseCountModeFilter(countArgs.name) local count = CleveRoids.CountEnemiesMatching(function(unit) - return not CleveRoids.IsUnitInMeleeRange(unit, true) + if CleveRoids.IsUnitInMeleeRange(unit, true) then return false end + if filter and not filter(unit) then return false end + return true end) return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) end - -- Original single-target behavior local unit = conditionals.target or "target" if not UnitExists(unit) then return true end - return not CleveRoids.IsUnitInMeleeRange(unit) end, diff --git a/Core.lua b/Core.lua index b1f8994..ebf34d9 100644 --- a/Core.lua +++ b/Core.lua @@ -70,14 +70,10 @@ local table_insert = table.insert local table_getn = table.getn -- Deferred stop-attack system (CheapShot pattern) --- ClearTarget() starves auto-attack (no target = can't swing). OnUpdate polls --- GetCurrentCastingInfo() — while autoattack is queued, keeps clearing target. --- Once autoattack drops off, retargets via stored GUID. Timeout prevents --- infinite loop if autoattack state gets stuck. +-- Stops autoattack and clears target. OnUpdate keeps clearing target while +-- autoattack is queued to prevent any swing from landing. No retarget. local _DeferStopFrame = CreateFrame("Frame") local _deferStopActive = false -local _deferRetargetGUID = nil -local _deferRetargetDone = false local _deferStartTime = 0 local DEFER_STOP_TIMEOUT = 0.5 @@ -87,30 +83,21 @@ _DeferStopFrame:SetScript("OnUpdate", function() return end - if _deferRetargetGUID and not _deferRetargetDone then - -- Timeout: retarget and stop regardless of autoattack state - if (GetTime() - _deferStartTime) >= DEFER_STOP_TIMEOUT then - TargetUnit(_deferRetargetGUID) - _deferRetargetDone = true - _deferRetargetGUID = nil - _deferStopActive = false - _DeferStopFrame:Hide() - return - end + -- Timeout: stop polling + if (GetTime() - _deferStartTime) >= DEFER_STOP_TIMEOUT then + _deferStopActive = false + _DeferStopFrame:Hide() + return + end - local _,_,_,_,_,_,autoattack = GetCurrentCastingInfo() - if autoattack == 1 then - if UnitExists("target") then - ClearTarget() - end - else - TargetUnit(_deferRetargetGUID) - _deferRetargetDone = true - _deferRetargetGUID = nil - _deferStopActive = false - _DeferStopFrame:Hide() + -- While autoattack is queued, keep clearing target to prevent swings + local _,_,_,_,_,_,autoattack = GetCurrentCastingInfo() + if autoattack == 1 then + if UnitExists("target") then + ClearTarget() end else + -- Autoattack stopped, done _deferStopActive = false _DeferStopFrame:Hide() end @@ -120,8 +107,6 @@ _DeferStopFrame:Hide() function CleveRoids.DeferStopAttack() CleveRoids.CurrentSpell.autoAttack = false CleveRoids.CurrentSpell.autoAttackLock = false - _deferRetargetGUID = CleveRoids.GetGUID("target") - _deferRetargetDone = false _deferStartTime = GetTime() -- Only toggle auto-attack OFF if it's currently active (avoid turning it ON) local attackSlot = CleveRoids.GetProxyActionSlot(CleveRoids.Localized.Attack) @@ -2014,9 +1999,14 @@ function CleveRoids.ParseMsg(msg) -- Split the conditional block by comma or space for _, conditionGroups in CleveRoids.splitStringIgnoringQuotes(conditionBlock, {",", " "}) do if conditionGroups ~= "" then - -- Split conditional groups by colon + -- Split conditional groups by colon (rejoin extra parts for multi-part args + -- e.g., distance:30:facing>1 → condition="distance", args="30:facing>1") local conditionGroup = CleveRoids.splitStringIgnoringQuotes(conditionGroups, ":") - local condition, args = conditionGroup[1], conditionGroup[2] + local condition = conditionGroup[1] + local args = conditionGroup[2] + for _cgi = 3, table.getn(conditionGroup) do + args = (args or "") .. ":" .. conditionGroup[_cgi] + end -- No args → the action is the implicit argument if not args or args == "" then diff --git a/NampowerAPI.lua b/NampowerAPI.lua index a8f5a8b..95b5a11 100644 --- a/NampowerAPI.lua +++ b/NampowerAPI.lua @@ -1064,15 +1064,17 @@ end function API.GetSpellRange(spellId) if not spellId or spellId == 0 then return nil end - -- First try rangeMax (Nampower may provide this as a resolved field) - local rangeMax = API.GetSpellField(spellId, "rangeMax") - if rangeMax and rangeMax > 0 then - return rangeMax / 10 -- Convert from game units to yards - end - - -- Fallback: lookup rangeIndex in SpellRange table local rangeIndex = API.GetSpellField(spellId, "rangeIndex") if rangeIndex then + -- Prefer GetSpellRangeData (v4.0+) for runtime DBC lookup (covers custom ranges) + if API.features.hasGetSpellRangeData and _G.GetSpellRangeData then + local minRange, maxRange, flags, name = _G.GetSpellRangeData(rangeIndex) + if maxRange and maxRange > 0 then + return maxRange + end + end + + -- Fallback: hardcoded SpellRange table local range = API.SpellRangeTable[rangeIndex] if range then return range