multiscan targeting based on pepopos cursive

This commit is contained in:
Jrc13245
2025-12-26 14:57:54 -05:00
parent d6144a7e83
commit 145ed92f77
5 changed files with 456 additions and 2 deletions
+354
View File
@@ -4769,3 +4769,357 @@ CleveRoids.Keywords = {
end, conditionals, "noresisted")
end
}
-- =============================================================================
-- MULTISCAN SYSTEM
-- =============================================================================
-- [multiscan:priority] - Scan nearby enemies and find best target matching priority
-- Requires UnitXP_SP3 for enemy enumeration
-- Uses SuperWoW for soft-casting (CastSpellByName with GUID)
-- Raid mark indices (WoW API GetRaidTargetIndex values)
CleveRoids.RAID_MARKS = {
star = 1, circle = 2, diamond = 3, triangle = 4,
moon = 5, square = 6, cross = 7, skull = 8,
}
-- Priority types for multiscan
-- String values = custom handling, number values = raid mark index
CleveRoids.MULTISCAN_PRIORITIES = {
-- Distance-based (use UnitXP direct targeting where possible)
nearest = "nearest",
farthest = "farthest",
-- Health percentage
highesthp = "highesthp",
lowesthp = "lowesthp",
-- Raw health value
highestrawhp = "highestrawhp",
lowestrawhp = "lowestrawhp",
-- Raid mark order (skull → cross → square → moon → triangle → diamond → circle → star)
markorder = "markorder",
-- Individual raid marks (direct unit reference via SuperWoW "mark#" tokens)
skull = 8, cross = 7, square = 6, moon = 5,
triangle = 4, diamond = 3, circle = 2, star = 1,
}
-- Static conditionals that don't depend on target (checked before scanning)
CleveRoids.STATIC_CONDITIONALS = {
group = true, nogroup = true,
combat = true, nocombat = true,
zone = true, subzone = true,
instance = true, noinstance = true,
stealth = true, nostealth = true,
form = true, noform = true, stance = true, nostance = true,
talent = true, notalent = true,
equipped = true, noequipped = true,
mod = true, nomod = true,
btn = true,
outdoors = true, indoors = true,
swimming = true, noswimming = true,
resting = true, noresting = true,
flying = true, noflying = true,
mounted = true, nomounted = true,
vehicle = true, novehicle = true,
bar = true,
}
--- Calculate score for a unit based on priority type
--- @param unit string Unit token or GUID
--- @param priority string Priority type (e.g., "nearest", "highesthp")
--- @param currentTargetGuid string|nil GUID of player's current target (exempt from combat check)
--- @param specifiedUnitGuid string|nil GUID of @unit specified in macro (also exempt from combat check)
--- @return number|nil Score (lower = better) or nil if invalid candidate
function CleveRoids.GetMultiscanScore(unit, priority, currentTargetGuid, specifiedUnitGuid)
if not unit or not UnitExists(unit) then return nil end
if UnitIsDeadOrGhost(unit) then return nil end
-- Must be attackable
if not UnitCanAttack("player", unit) then return nil end
-- Combat check: must be in combat with player, UNLESS it's current target OR specified @unit
local _, unitGuid = UnitExists(unit)
local isCurrentTarget = currentTargetGuid and unitGuid == currentTargetGuid
local isSpecifiedUnit = specifiedUnitGuid and unitGuid == specifiedUnitGuid
if not isCurrentTarget and not isSpecifiedUnit and not UnitAffectingCombat(unit) then
return nil
end
-- Calculate score based on priority
if priority == "nearest" then
if CleveRoids.hasUnitXP then
local distance = UnitXP("distanceBetween", "player", unit)
return distance or 9999
end
return 0 -- No distance info, treat as equal
elseif priority == "farthest" then
if CleveRoids.hasUnitXP then
local distance = UnitXP("distanceBetween", "player", unit)
return distance and -distance or -9999
end
return 0
elseif priority == "highesthp" then
local maxHp = UnitHealthMax(unit)
if maxHp <= 0 then return nil end
local hpPct = 100 * UnitHealth(unit) / maxHp
return -hpPct -- Negative so highest becomes lowest score
elseif priority == "lowesthp" then
local maxHp = UnitHealthMax(unit)
if maxHp <= 0 then return nil end
local hpPct = 100 * UnitHealth(unit) / maxHp
return hpPct
elseif priority == "highestrawhp" then
local rawHp = UnitHealth(unit)
return -rawHp -- Negative so highest becomes lowest score
elseif priority == "lowestrawhp" then
local rawHp = UnitHealth(unit)
return rawHp
end
return 0 -- Unknown priority, treat as equal
end
--- Check if a candidate passes all target-dependent conditionals
--- @param conditionals table The conditionals table
--- @param candidateUnit string Unit token or GUID to test
--- @return boolean True if candidate passes all conditionals
function CleveRoids.ValidateMultiscanCandidate(conditionals, candidateUnit)
local originalTarget = conditionals.target
conditionals.target = candidateUnit
local passes = true
for k, _ in pairs(conditionals) do
-- Skip static conditionals (already checked) and metadata
if not CleveRoids.ignoreKeywords[k] and not CleveRoids.STATIC_CONDITIONALS[k] then
local fn = CleveRoids.Keywords[k]
if fn and not fn(conditionals) then
passes = false
break
end
end
end
conditionals.target = originalTarget
return passes
end
--- Check static conditionals before scanning
--- @param conditionals table The conditionals table
--- @return boolean True if all static conditionals pass
function CleveRoids.CheckStaticConditionals(conditionals)
for k, _ in pairs(conditionals) do
if CleveRoids.STATIC_CONDITIONALS[k] then
local fn = CleveRoids.Keywords[k]
if fn and not fn(conditionals) then
return false
end
end
end
return true
end
--- Main multiscan resolution function
--- Scans enemies using UnitXP, finds best target matching priority and conditionals
--- @param conditionals table The conditionals table containing multiscan value
--- @param specifiedUnit string|nil The @unit specified in the macro (exempt from combat check)
--- @return string|nil GUID of best target, or nil if none found
function CleveRoids.ResolveMultiscanTarget(conditionals, specifiedUnit)
-- Require UnitXP for enemy scanning
if not CleveRoids.hasUnitXP then
return nil
end
-- Parse priority from conditionals.multiscan
local priority = nil
if type(conditionals.multiscan) == "table" then
priority = conditionals.multiscan[1]
elseif type(conditionals.multiscan) == "string" then
priority = conditionals.multiscan
end
if not priority then return nil end
priority = string.lower(CleveRoids.Trim(priority))
-- Validate priority type
local priorityType = CleveRoids.MULTISCAN_PRIORITIES[priority]
if not priorityType then
CleveRoids.Print("|cffff0000[multiscan]|r Unknown priority: " .. priority)
return nil
end
-- Check static conditionals first (group, combat, zone, etc.)
if not CleveRoids.CheckStaticConditionals(conditionals) then
return nil
end
-- Save current target for restoration and combat-check exemption
local currentTargetGuid = nil
if UnitExists("target") then
local _, guid = UnitExists("target")
currentTargetGuid = guid
end
-- Resolve specified @unit GUID (also exempt from combat check)
local specifiedUnitGuid = nil
if specifiedUnit and UnitExists(specifiedUnit) then
local _, guid = UnitExists(specifiedUnit)
specifiedUnitGuid = guid
end
-- Handle raid mark priorities (direct unit reference)
if type(priorityType) == "number" then
local markUnit = "mark" .. priorityType
if UnitExists(markUnit) and UnitCanAttack("player", markUnit) then
-- Validate against target-dependent conditionals
if CleveRoids.ValidateMultiscanCandidate(conditionals, markUnit) then
local _, guid = UnitExists(markUnit)
return guid
end
end
return nil -- Raid mark not found or doesn't pass conditionals
end
-- Handle markorder priority (skull → cross → square → moon → triangle → diamond → circle → star)
if priorityType == "markorder" then
-- Iterate marks from skull (8) down to star (1) in standard kill order
for markIndex = 8, 1, -1 do
local markUnit = "mark" .. markIndex
if UnitExists(markUnit) and UnitCanAttack("player", markUnit) then
-- Validate against target-dependent conditionals
if CleveRoids.ValidateMultiscanCandidate(conditionals, markUnit) then
local _, guid = UnitExists(markUnit)
return guid
end
end
end
return nil -- No valid marked target found
end
-- Handle UnitXP direct targeting for simple priorities
if priorityType == "nearest" then
-- If no specified unit, use UnitXP's nearestEnemy directly for efficiency
if not specifiedUnitGuid then
local found = UnitXP("target", "nearestEnemy")
if found and UnitExists("target") then
local _, foundGuid = UnitExists("target")
-- Restore original target
if currentTargetGuid then
TargetUnit(currentTargetGuid)
else
ClearTarget()
end
-- Validate the found target
if CleveRoids.ValidateMultiscanCandidate(conditionals, foundGuid) then
return foundGuid
end
end
-- Restore target if nearestEnemy failed
if currentTargetGuid then
TargetUnit(currentTargetGuid)
else
ClearTarget()
end
return nil
end
-- Fall through to custom scan if there's a specified unit to consider
end
if priorityType == "highesthp" then
-- If no specified unit, try UnitXP's mostHP directly for efficiency
if not specifiedUnitGuid then
local found = UnitXP("target", "mostHP")
if found and UnitExists("target") then
local _, foundGuid = UnitExists("target")
-- Restore original target
if currentTargetGuid then
TargetUnit(currentTargetGuid)
else
ClearTarget()
end
-- Validate the found target
if CleveRoids.ValidateMultiscanCandidate(conditionals, foundGuid) then
return foundGuid
end
end
-- Restore target if mostHP failed validation
if currentTargetGuid then
TargetUnit(currentTargetGuid)
else
ClearTarget()
end
end
-- Fall through to custom scan if there's a specified unit or built-in didn't pass validation
end
-- Custom scan for complex priorities (lowesthp, highestrawhp, lowestrawhp, farthest)
-- Also used as fallback if UnitXP direct targeting didn't pass validation
local candidates = {} -- { { guid = X, score = Y }, ... }
local seenGuids = {}
-- Helper to evaluate a candidate
local function evaluateCandidate(unit)
if not UnitExists(unit) then return end
local _, guid = UnitExists(unit)
if not guid or seenGuids[guid] then return end
seenGuids[guid] = true
local score = CleveRoids.GetMultiscanScore(unit, priorityType, currentTargetGuid, specifiedUnitGuid)
if not score then return end
-- Validate against target-dependent conditionals
if CleveRoids.ValidateMultiscanCandidate(conditionals, guid) then
table.insert(candidates, { guid = guid, score = score })
end
end
-- Always consider current target (exempt from combat check via GetMultiscanScore)
if currentTargetGuid then
evaluateCandidate("target")
end
-- Also consider specified @unit (exempt from combat check via GetMultiscanScore)
if specifiedUnitGuid and specifiedUnitGuid ~= currentTargetGuid then
evaluateCandidate(specifiedUnit)
end
-- Scan via UnitXP enemy iteration
local firstGuid = nil
local maxIterations = 50
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 = UnitExists("target")
if not currentGuid then break end
if firstGuid == nil then
firstGuid = currentGuid
elseif currentGuid == firstGuid then
break -- Completed full cycle
end
evaluateCandidate("target")
end
-- Restore original target
if currentTargetGuid then
TargetUnit(currentTargetGuid)
else
ClearTarget()
end
-- Find best candidate (lowest score wins)
if table.getn(candidates) == 0 then
return nil
end
table.sort(candidates, function(a, b) return a.score < b.score end)
return candidates[1].guid
end
+16
View File
@@ -2131,6 +2131,22 @@ function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBefo
end
end
-- Handle [multiscan:priority] - scan enemies and find best target
-- Must be processed BEFORE the Keywords loop since it sets conditionals.target
-- Pass origTarget so @unit syntax makes that unit exempt from combat check
if conditionals.multiscan then
local scanResult = CleveRoids.ResolveMultiscanTarget(conditionals, origTarget)
if not scanResult then
-- No valid target found - fail this conditional line (try next)
conditionals.target = origTarget
return false
end
-- Set target to the found GUID for soft-casting via SuperWoW
conditionals.target = scanResult
-- Don't need to retarget since we're using GUID directly
needRetarget = false
end
for k, v in pairs(conditionals) do
if not CleveRoids.ignoreKeywords[k] then
local result = CleveRoids.Keywords[k] and CleveRoids.Keywords[k](conditionals)
+1
View File
@@ -133,6 +133,7 @@ CleveRoids.ignoreKeywords = {
cancelaura = true,
_operators = true, -- Metadata for AND/OR operator tracking
_groups = true, -- Grouped conditional values for AND/OR evaluation
multiscan = true, -- Processed before Keywords loop (target resolution)
}
-- TODO: Localize?
+83
View File
@@ -577,6 +577,7 @@ Default `@unitid` is usually `@target` if not specified.
| level | [level:>=60] | * | | Unit level |
| meleerange | [meleerange] | | * | In melee range |
| member | [member] | * | | In party OR raid |
| multiscan | [multiscan:priority] | * | | Scan for best enemy target |
| notarget | [notarget] | | | Player has no target |
| outrange | [outrange:"Name"] | | | Out of spell range |
| party | [party]/[party:unitid] | | * | Unit in party |
@@ -682,6 +683,88 @@ Crowd control detection using DBC spell mechanic data (785 spells).
`[cc]` or `[nocc]` without a type checks for any **loss-of-control** effect:
stun, fear, sleep, charm, polymorph, banish, horror, freeze, disorient, shackle
## Multiscan (Target Scanning) - *Credits to Pepopo and the Cursive addon*
Scans nearby enemies using UnitXP and finds the best target matching the specified priority. Uses SuperWoW for **soft-casting** (no target change required).
**Requirements:**
- UnitXP_SP3 for enemy enumeration
- SuperWoW for GUID-based casting
### Syntax
```lua
[multiscan:priority] -- Find best enemy by priority
[multiscan:raidmark] -- Target specific raid mark
```
### Available Priorities
| Priority | Description |
|----------|-------------|
| `nearest` | Closest enemy (UnitXP nearestEnemy) |
| `farthest` | Farthest enemy |
| `highesthp` | Highest health % (UnitXP mostHP) |
| `lowesthp` | Lowest health % |
| `highestrawhp` | Highest raw HP value |
| `lowestrawhp` | Lowest raw HP value |
| `markorder` | First valid raid mark in kill order (skull→cross→square→moon→triangle→diamond→circle→star) |
| `skull` | Enemy with Skull raid mark |
| `cross` | Enemy with Cross raid mark |
| `square` | Enemy with Square raid mark |
| `moon` | Enemy with Moon raid mark |
| `triangle` | Enemy with Triangle raid mark |
| `diamond` | Enemy with Diamond raid mark |
| `circle` | Enemy with Circle raid mark |
| `star` | Enemy with Star raid mark |
### Combat Requirement
Scanned targets must be **in combat with the player** (UnitXP default behavior), with these exceptions:
- Player's current target is always considered
- `@unit` specified in the macro is always considered (e.g., `[@targettarget,multiscan:nearest]`)
### Soft Targeting
Uses SuperWoW's `CastSpellByName(spell, guid)` to cast on the scanned target **without changing the player's actual target**. Your target frame stays unchanged while spells are cast on the optimal enemy.
### Examples
```lua
-- Solo: scan for nearest enemy without Rake
/cast [nogroup,multiscan:nearest,nodebuff:Rake] Rake
-- Group: priority on skull-marked target
/cast [group,multiscan:skull,harm] Eviscerate
-- Highest HP without Rake, check immunity
/cast [multiscan:highesthp,nodebuff:Rake,noimmune] Rake
-- Use targettarget (exempt from combat check)
/cast [@targettarget,multiscan:nearest] Rake
-- Multi-DoT with multiscan
/cast [multiscan:nearest,nodebuff:Moonfire] Moonfire
/cast [multiscan:nearest,nodebuff:Insect_Swarm] Insect Swarm
/cast Wrath
-- Focus fire marked targets in kill order (skull first, then cross, etc.)
/cast [group,multiscan:markorder] Sinister Strike
/cast [nogroup,multiscan:lowesthp] Sinister Strike
-- Markorder with conditionals - attacks highest priority mark that's low HP
/cast [multiscan:markorder,hp:<30] Execute
```
### Group Integration
Combine with `[group]`/`[nogroup]` to switch behavior based on context:
```lua
-- Different priorities for solo vs group
/cast [nogroup,multiscan:nearest] Rake -- Solo: nearest target
/cast [group,multiscan:skull] Rake -- Group: skull priority
```
---
# Supported Addons
+2 -2
View File
@@ -1,8 +1,8 @@
## Interface: 11200
## Author: DWG, _brain, Torio, yani9o
## Author: DWG, _brain, Torio, yani9o, Pepopo
## Title: [Super]CleveRoidMacros
## Notes: /cleveroid for settings
## Version: 1.6
## Version: 1.7
## OptionalDeps: ClassicFocus, FocusFrame, pfUI, SuperMacro, Bongos_ActionBar
## SavedVariables: CleveRoidMacros, CleveRoids_LearnedDurations, CleveRoids_AuraTextures, CleveRoids_ImmunityData, CleveRoids_ComboDurations, CleveRoids_SpellSchools
Localization.lua