fix meleerange and count mode

This commit is contained in:
Jrc13245
2026-03-08 16:38:57 -04:00
parent b4a442e23a
commit 69b9c08a2b
3 changed files with 127 additions and 91 deletions
+103 -86
View File
@@ -2851,62 +2851,101 @@ end
-- Enables conditionals like [meleerange:>1], [behind:>=2], [inrange:Spell>1]
-- Uses UnitXP to enumerate nearby enemies and count those matching a condition
--- Count enemies matching a condition function
--- Uses UnitXP enemy iteration with cycle detection and target restoration
--- Persistent set of known enemy GUIDs.
--- Populated by nameplate scans, unit token checks, and PLAYER_TARGET_CHANGED.
--- Cleaned up on UNIT_DIED and ZONE_CHANGED_NEW_AREA.
--- Stale entries are naturally filtered by UnitExists(guid) at query time.
CleveRoids.knownEnemyGuids = {}
--- Count enemies matching a condition function.
--- Enumerates nearby enemies via nameplate GUIDs, unit tokens (party/raid targets,
--- focus, targettarget, etc.), and previously-seen GUIDs. Never switches the
--- player's target, so combo points and target state are preserved.
--- @param checkFunc function(unit) -> boolean - Returns true if unit matches condition
--- @return number - Count of matching enemies (0 if UnitXP unavailable)
--- @return number - Count of matching enemies (0 if no detection method available)
function CleveRoids.CountEnemiesMatching(checkFunc)
if not CleveRoids.hasUnitXP then return 0 end
-- Save current target for restoration
local currentTargetGuid = nil
if UnitExists("target") then
currentTargetGuid = CleveRoids.GetGUID("target")
end
local count = 0
local seenGuids = {}
local firstGuid = nil
local maxIterations = 50
local checked = {}
-- Always count current target first (if valid and matches)
if currentTargetGuid and UnitCanAttack("player", "target") then
-- 1. Current target (use "target" token for best API compatibility)
if UnitExists("target") and UnitCanAttack("player", "target") then
local guid = CleveRoids.GetGUID("target")
if guid then
checked[guid] = true
CleveRoids.knownEnemyGuids[guid] = true
end
if checkFunc("target") then
count = count + 1
end
seenGuids[currentTargetGuid] = true
end
-- Iterate through enemies using UnitXP
for i = 1, maxIterations do
local found = UnitXP("target", "nextEnemyConsideringDistance")
if not found then break end
if not UnitExists("target") then break end
local currentGuid = CleveRoids.GetGUID("target")
if not currentGuid then break end
-- Cycle detection: stop when we see the first target again
if firstGuid == nil then
firstGuid = currentGuid
elseif currentGuid == firstGuid then
break
-- Helper: check a unit token as an attackable enemy
local function tryUnit(unit)
if not UnitExists(unit) then return end
if not UnitCanAttack("player", unit) then return end
local guid = CleveRoids.GetGUID(unit)
if not guid or checked[guid] then return end
checked[guid] = true
CleveRoids.knownEnemyGuids[guid] = true
if checkFunc(unit) then
count = count + 1
end
end
-- Skip already-counted targets
if not seenGuids[currentGuid] then
seenGuids[currentGuid] = true
if UnitCanAttack("player", "target") and checkFunc("target") then
count = count + 1
-- 2. Nearby unit tokens (no target switching)
tryUnit("targettarget")
tryUnit("pettarget")
if pfUI and pfUI.uf and pfUI.uf.focus and pfUI.uf.focus.label and pfUI.uf.focus.id then
tryUnit(pfUI.uf.focus.label .. pfUI.uf.focus.id)
end
for i = 1, 4 do
tryUnit("party" .. i .. "target")
end
if GetNumRaidMembers() > 0 then
for i = 1, 40 do
tryUnit("raid" .. i .. "target")
end
end
-- 3. Nameplate scan: visible nameplates give live GUIDs without target switching
-- Requires Nampower v2.28+ (hasNameplateGUID) or SuperWoW
local API = CleveRoids.NampowerAPI
if (API and API.features.hasNameplateGUID) or CleveRoids.hasSuperwow then
local numChildren = WorldFrame:GetNumChildren()
local children = { WorldFrame:GetChildren() }
for i = 1, numChildren do
local frame = children[i]
if frame and frame:IsVisible() then
local success, guid = pcall(frame.GetName, frame, 1)
if success and guid and type(guid) == "string" and string.len(guid) > 0 and not checked[guid] then
if UnitExists(guid) and UnitCanAttack("player", guid) then
checked[guid] = true
CleveRoids.knownEnemyGuids[guid] = true
if checkFunc(guid) then
count = count + 1
end
end
end
end
end
end
-- Restore original target
if currentTargetGuid then
TargetUnit(currentTargetGuid)
else
ClearTarget()
-- During tooltip evaluation, skip known-enemy cache iteration.
-- Layers 1-3 (target + unit tokens + nameplates) cover visible enemies.
if CleveRoids._isTestingAction then
return count
end
-- 4. Previously-seen enemies (stealthed units, enemies that left nameplate range, etc.)
for guid, _ in pairs(CleveRoids.knownEnemyGuids) do
if not checked[guid] then
if UnitExists(guid) and UnitCanAttack("player", guid) then
checked[guid] = true
if checkFunc(guid) then
count = count + 1
end
end
end
end
return count
@@ -2918,11 +2957,6 @@ end
function CleveRoids.GetCountModeArgs(conditionalValue)
if type(conditionalValue) ~= "table" then return nil end
-- During tooltip/icon evaluation (TestAction), skip count mode to avoid
-- target cycling side effects from CountEnemiesMatching. Keyword handlers
-- fall through to their single-target behavior instead.
if CleveRoids._isTestingAction then return nil end
local args = conditionalValue[1]
if type(args) == "table" and args.operator and args.amount then
return args
@@ -5329,32 +5363,30 @@ local function FindMeleeRangeSpell()
end
--- Check if a unit is in melee range.
--- @param unit string Unit token to check
--- @param targetCycling boolean? When true (inside CountEnemiesMatching),
--- IsSpellInRange returns stale results so UnitXP distance is preferred.
--- @param unit string Unit token or GUID to check
--- @param cleaveRange boolean? When true, uses 5-yard threshold (cleave/splash range)
--- instead of 2-yard threshold (auto-attack melee range). Used by count mode to
--- detect enemies within cleave distance.
--- @return boolean
function CleveRoids.IsUnitInMeleeRange(unit, targetCycling)
function CleveRoids.IsUnitInMeleeRange(unit, cleaveRange)
if not UnitExists(unit) then return false end
-- UnitXP meleeAutoAttack distance is most accurate for melee range.
-- Default distanceBetween measures ranged spell distance which doesn't
-- account for hitbox sizes correctly for melee.
local threshold = cleaveRange and 5 or 2
if CleveRoids.hasUnitXP then
local distance = UnitXP("distanceBetween", "player", unit, "meleeAutoAttack")
if distance then return distance <= 5 end
local distance = UnitXP("distanceBetween", "player", unit)
if distance then return distance <= threshold end
end
-- Fallback: IsSpellInRange with a melee-range spell
if not targetCycling then
local spellId = FindMeleeRangeSpell()
if spellId then
local result = IsSpellInRange(spellId, unit)
if result == 1 then return true end
if result == 0 then return false end
end
-- Fallback: IsSpellInRange with a melee-range spell (works with GUID tokens)
local spellId = FindMeleeRangeSpell()
if spellId then
local result = IsSpellInRange(spellId, unit)
if result == 1 then return true end
if result == 0 then return false end
end
-- Last resort
-- Last resort (~10 yards, less accurate)
return CheckInteractDistance(unit, 3)
end
@@ -6645,8 +6677,6 @@ CleveRoids.Keywords = {
-- Check for count mode: [inrange:Multi-Shot>1]
if type(args) == "table" and args.operator and args.amount then
local spellName = args.name or conditionals.action
-- Resolve spell range once outside the loop for UnitXP distance checks
-- IsSpellInRange may return stale results during UnitXP target iteration
local spellId = API.GetSpellIdFromName(spellName)
local spellRange = spellId and API.GetSpellRange(spellId)
local count = CleveRoids.CountEnemiesMatching(function(unit)
@@ -6656,8 +6686,7 @@ CleveRoids.Keywords = {
return distance <= spellRange
end
end
local result = API.IsSpellInRange(spellName, unit)
return result == 1
return API.IsSpellInRange(spellName, unit) == 1
end)
return CleveRoids.comparators[args.operator](count, args.amount)
end
@@ -6665,10 +6694,7 @@ CleveRoids.Keywords = {
-- Original single-target behavior
local target = conditionals.target or "target"
local checkValue = (type(args) == "string" and args) or conditionals.action
-- Use API wrapper which handles self-cast spells correctly
local result = API.IsSpellInRange(checkValue, target)
return result == 1
return API.IsSpellInRange(checkValue, target) == 1
end, conditionals, "inrange")
end,
@@ -6690,8 +6716,7 @@ CleveRoids.Keywords = {
return distance > spellRange
end
end
local result = API.IsSpellInRange(spellName, unit)
return result == 0
return API.IsSpellInRange(spellName, unit) == 0
end)
return CleveRoids.comparators[args.operator](count, args.amount)
end
@@ -6699,10 +6724,7 @@ CleveRoids.Keywords = {
-- Original single-target behavior
local target = conditionals.target or "target"
local checkValue = (type(args) == "string" and args) or conditionals.action
-- Use API wrapper which handles self-cast spells correctly
local result = API.IsSpellInRange(checkValue, target)
return result == 0
return API.IsSpellInRange(checkValue, target) == 0
end, conditionals, "noinrange")
end,
@@ -6724,8 +6746,7 @@ CleveRoids.Keywords = {
return distance > spellRange
end
end
local result = API.IsSpellInRange(spellName, unit)
return result == 0
return API.IsSpellInRange(spellName, unit) == 0
end)
return CleveRoids.comparators[args.operator](count, args.amount)
end
@@ -6733,10 +6754,7 @@ CleveRoids.Keywords = {
-- Original single-target behavior
local target = conditionals.target or "target"
local checkValue = (type(args) == "string" and args) or conditionals.action
-- Use API wrapper which handles self-cast spells correctly
local result = API.IsSpellInRange(checkValue, target)
return result == 0
return API.IsSpellInRange(checkValue, target) == 0
end, conditionals, "outrange")
end,
@@ -6758,8 +6776,7 @@ CleveRoids.Keywords = {
return distance <= spellRange
end
end
local result = API.IsSpellInRange(spellName, unit)
return result == 1
return API.IsSpellInRange(spellName, unit) == 1
end)
return CleveRoids.comparators[args.operator](count, args.amount)
end
+2 -4
View File
@@ -2302,10 +2302,8 @@ function CleveRoids.TestAction(cmd, args)
CleveRoids.FixEmptyTarget(conditionals)
-- FIX: Set flag to prevent side effects (target cycling) during tooltip evaluation.
-- CountEnemiesMatching changes the player's actual target via UnitXP scanning,
-- which fires PLAYER_TARGET_CHANGED events and interferes with macro execution
-- when called from the periodic action bar update (TestForActiveAction).
-- Flag for tooltip evaluation. CountEnemiesMatching skips expensive
-- nameplate scan and known-enemy cache, using only target + unit tokens.
CleveRoids._isTestingAction = true
-- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation
+22 -1
View File
@@ -3853,6 +3853,7 @@ local ev = CreateFrame("Frame", "CleveRoidsLibDebuffFrame", UIParent)
ev:RegisterEvent("PLAYER_TARGET_CHANGED")
ev:RegisterEvent("UNIT_AURA")
ev:RegisterEvent("ADDON_LOADED") -- For pfUI integration initialization
ev:RegisterEvent("ZONE_CHANGED_NEW_AREA") -- Clear known enemy GUIDs on zone change
if CleveRoids.hasSuperwow then
ev:RegisterEvent("UNIT_CASTEVENT")
@@ -3919,7 +3920,23 @@ ev:SetScript("OnEvent", function()
return
end
if event == "PLAYER_TARGET_CHANGED" then
if event == "ZONE_CHANGED_NEW_AREA" then
-- Wipe known enemy GUIDs — all previous units are invalid in the new zone
if CleveRoids.knownEnemyGuids then
for k in pairs(CleveRoids.knownEnemyGuids) do
CleveRoids.knownEnemyGuids[k] = nil
end
end
return
elseif event == "PLAYER_TARGET_CHANGED" then
-- Seed known enemy GUIDs from target changes
if UnitExists("target") and UnitCanAttack("player", "target") and CleveRoids.knownEnemyGuids then
local guid = CleveRoids.GetGUID("target")
if guid then
CleveRoids.knownEnemyGuids[guid] = true
end
end
SeedUnit("target")
elseif event == "UNIT_AURA" and arg1 == "target" then
@@ -5624,6 +5641,10 @@ ev:SetScript("OnEvent", function()
if CleveRoids.AllCasterAuraTracking and CleveRoids.AllCasterAuraTracking[guid] then
CleveRoids.AllCasterAuraTracking[guid] = nil
end
-- Remove from known enemy GUID tracker
if CleveRoids.knownEnemyGuids then
CleveRoids.knownEnemyGuids[guid] = nil
end
-- Clean up GUID to name mapping (after 5 seconds to allow final lookups)
-- Actually, keep it for a bit in case we need it for immunity detection