Property Implementation Reference
Early WA1 shipped nearly a hundred preset types in the property dropdown (removed in 2026-08, when the property system switched to “custom code + property library”). This document collects every old implementation from earlier releases and adapts it to the current WA1 as paste-ready custom code, for module authors’ reference. Client: both retail (Midnight) and Classic are supported and share one code path. The code below is written against the retail API surface and secret rules. Secret sanitizing (
SuperDPS.SN) is a side-effect-free no-op on Classic, so write it as-is; but retail-only interfaces (such asC_AssistedCombat) return nothing on Classic, and a fewC_Spellstruct fields may differ between the two, so verify in-game once before moving code to Classic.Sources: the property table and the addon query function library of earlier WA1 releases, compiled 2026-08-19.
How to use this in the current WA1
Section titled “How to use this in the current WA1”- Module editor → property card → choose “Custom code” in the property dropdown (or pick a ready-made entry from the property library).
- Click “Edit” and paste the code from this document. Fill the Parameters column with comma-separated values (up to 3 segments); the code reads them as
arg1/arg2/arg3. Purely numeric segments are passed as numbers, everything else as strings, and empty segments arenil. - The code’s
returnvalue is sent back to the desktop side over the strip, the same as any other property.
One piece of code can be reused by several property rows (write arg1 in the code, then fill player / target as the parameter on two different rows).
Property code vs. init code (performance rules)
Section titled “Property code vs. init code (performance rules)”- Property code is a high-frequency hot path: it runs every 0.02 s, once per property row. Only do reads and the necessary queries there. Do not put one-time work in it (
CreateFrame/RegisterEvent, building shared tables, defining shared functions, and so on). Even behind anif not ...guard the check still runs every frame for nothing, and forgetting the guard leaks one frame object per tick. - One-time work belongs in the module’s “init code” (module editor → Init code button). It runs once when the addon loads, before property registration, in the same environment as property code (same chunk, with the compatibility layer and the
SuperDPS.*interfaces available). It is the right place to create event frames, initialize shared tables and define shared functions. Every entry in this document that needs event tracking (codes 1/6/12) or shares heavy logic across several rows (code 22) is given as two parts: “init code + property code”. - Keeping the
if not SuperDPS.XxxFrameguard in init code is deliberate: init code runs again when the addon is reinstalled or reloaded, frame objects cannot be destroyed, and the guard prevents duplicate creation and double counting. That check runs only once at load time, with no hot-path cost. - An error thrown in init code aborts the whole addon startup (it is not inside the pcall protection that wraps properties), so watch for nil guards inside event callbacks and keep the logic robust.
Migrating old modules: in old .wa1 files, the "p" key of a property row is the “Old Key” in the tables below. After loading in the new version these rows are classified as custom code, but their value is still the old “parameter” rather than code. Add the code from this document and move the old value into the Parameters column.
Environment available to custom code
Section titled “Environment available to custom code”- Same chunk as the addon, so WoW APIs can be called directly.
GetSpellInfo/GetSpellCooldown/GetSpellCharges/UnitAura/UnitBuff/UnitDebuffare compatibility wrappers (old multi-return signatures) over the newC_Spell/C_UnitAurasinterfaces. - Reserved interfaces:
SuperDPS.GetUnitAura/GetUnitBuff/GetUnitDebuff(look up an aura by name or spell ID; duration/expirationTime/count already sanitized),SuperDPS.SN(v, "tag")(protected-value sanitizing, protected values become 0),SuperDPS.HekeliPrimary,SuperDPS.Prop(shared value table). - Inside the function body,
UnitHealth/UnitHealthMax/UnitPower/UnitPowerMaxare the native raw versions and may return protected values in combat.
The three Midnight value-protection rules (the main reason old implementations broke)
Section titled “The three Midnight value-protection rules (the main reason old implementations broke)”- Pass-through, passing as arguments and truthiness checks are fine: a protected value can be
returned directly (the strip carries it back as usual), used inX or 0, or tested withif not X(non-boolean). - Comparison and arithmetic require sanitizing first:
>,==and the four arithmetic operators throw immediately on a protected value. Run it throughSuperDPS.SN(v, "tag")first (protected values become 0). - Exception:
isActive/isOnGCDfromC_Spell.GetSpellCooldown()are officially guaranteed never to be protected and are safe for logic.
The many %255, ×100, /100 scalings in the old implementations are leftovers from the old single-byte “pixel color block” channel. The new strip carries full values, so they have all been removed. When reusing thresholds from old modules, mind the unit conversion (noted per entry).
Legend: 📦 = a preset entry already exists in the new property library, use it directly; ✅ = use the numbered code below; ❌ = cannot be implemented under Midnight, reason noted.
1. Basics
Section titled “1. Basics”| Old property | Old Key | Old implementation | New version |
|---|---|---|---|
| In Combat | PropC4Ca | SuperDPS.InCombatLockdown() |
📦 preset “In Combat” |
| Behind Target | PropC4Cb | getBehind() (UI_ERROR_MESSAGE event tracking) |
✅ Code 1 |
| Global Cooldown | PropC81E | GCD() (cooldown of 61304 ×100, >255 becomes 0) |
📦 preset “Global Cooldown” (unit changed to seconds) |
| WA global property | PropC81F | WaProp(name) reads SuperDPS.Prop |
📦 preset “Global Property” |
| Hekili recommendation | PropC82F | Dedicated client-side channel (removed) | 📦 preset “Hekili Recommendation”; there is also the zero-dependency “Assisted Combat Recommendation” |
Code 1: Behind Target (parameters: none). The old version relied on the addon’s main event frame recording the “must be behind the target” error; the new addon no longer registers that event, so the module creates its own event frame. Semantics unchanged (treated as not behind for 0.6 s after the error).
Put this in the init code:
if not SuperDPS.BehindFrame then SuperDPS.BehindAt = 0 local f = CreateFrame("Frame") f:RegisterEvent("UI_ERROR_MESSAGE") f:SetScript("OnEvent", function(_, _, _, msg) if msg == SPELL_FAILED_NOT_BEHIND then SuperDPS.BehindAt = GetTime() end end) SuperDPS.BehindFrame = fendProperty code:
if GetTime() - SuperDPS.BehindAt > 0.6 then return 1 endreturn 0The old Global CD returned “centiseconds” (remaining seconds ×100, values above 255 became 0); the preset “Global Cooldown” returns seconds. Divide old thresholds by 100 to reuse them.
2. Health
Section titled “2. Health”| Old property | Old Key | Old implementation | New version |
|---|---|---|---|
| own health % | PropEccb | getHeal("player") (Health/Max×100) |
📦 preset “Health Percentage” (0~1 fraction) or ✅ Code 2 |
| target health % | PropA87F | getHeal("target") |
Same, parameter target |
| pet health % | Prop1679 | getHeal("pet") |
Same, parameter pet |
| target actual health, thousands | PropE4Da | getHeal2 = UnitHealth/1000 |
✅ Code 3 (pass the raw value through; division hits protection) |
| target actual health, ten-thousands | PropE4Db | getHeal3 = UnitHealth/10000 |
Same as Code 3 |
The old UnitHealth/UnitHealthMax division throws outright in combat on Midnight (protected values cannot be used in arithmetic).
Code 2: Health percent (old 0~100 scale) (parameters: player / target / pet). The value can only be multiplied by 100 after SN sanitizing; if it is protected in combat it counts as 0. If a 0~1 scale is acceptable, prefer the preset “Health Percentage” (pass-through, not subject to the sanitizing downgrade):
local g = UnitGUID(arg1)if not g then return 0 endlocal v = SuperDPS.SN(UnitPercentHealthFromGUID(g), "healpct", arg1)return v * 100Code 3: Actual health (parameter: target). Returns the raw value directly (protected values can still ride the strip); do the thousands/ten-thousands conversion on the desktop side:
return UnitHealth(arg1)3. Power and range
Section titled “3. Power and range”| Old property | Old Key | Old implementation | New version |
|---|---|---|---|
| own primary power | Prop8F14 | getPower("player") (percent) |
✅ Code 4 (percent, downgrades) or 📦 “Energy” (pass-through) |
| own power by type | PropC9F0 | UnitPower("player", type) |
📦 preset “Energy” / “Combo Points”, parameters player,type |
| target power by type | Prop45C4 | UnitPower("target", type) |
Same, parameters target,type |
| own power % | PropD3D9 | getPowerById("player", type) |
✅ Code 4 |
| target power % | Prop6512 | getPowerById("target", type) |
✅ Code 4 |
| target range | Prop45C5 | getTargetRange() (probes several APIs in turn) |
✅ Code 5 (downgrades) or 📦 “In Spell Range” |
| target castable, line of sight | Prop45C6 | UnitInLos2() (error event tracking) |
✅ Code 6 |
Power type mapping (from the old dropdown hint): -2 HealthCost, 0 Mana, 1 Rage, 2 Focus, 3 Energy, 4 Combo Points, 5 Runes, 6 Runic Power, 7 Soul Shards, 8 Lunar Power, 9 Holy Power, 11 Maelstrom, 12 Chi, 13 Insanity, 16 Arcane Charges, 17 Fury, 18 Pain; leave empty = current primary resource.
Code 4: Power percent (parameter 1: unit; parameter 2: type, optional). If both values are protected in combat the result is 0. The new property library already has a preset “Power Percentage” 📦: on retail it passes UnitPowerPercent through when available and only falls back to the division below otherwise. Prefer the pass-through version and do the comparison on the desktop side:
local p = SuperDPS.SN(UnitPower(arg1, arg2), "powpct", arg1)local m = SuperDPS.SN(UnitPowerMax(arg1, arg2), "powpct", arg1)if m == 0 then return 0 endreturn p / m * 100Code 5: Target range (parameters: none; returns 255 when there is no target or the value cannot be read, same as the old semantics). UnitDistanceSquared may be restricted for hostile units on Midnight, so availability depends on in-game testing. To check range for a specific spell, use the preset “In Spell Range” instead:
if not UnitExists("target") then return 255 endlocal ok, d2 = pcall(UnitDistanceSquared, "target")if ok then d2 = SuperDPS.SN(d2, "range") if d2 > 0 then return math.sqrt(d2) endendreturn 255Code 6: Target castable (line of sight / out of range) (parameters: none). Old semantics: the target exists and is visible, and no “out of range / line of sight” error has been reported against it within the last 1.5 s.
Put this in the init code (sharing one frame with Code 1 is fine too: one frame can register several events and branch in the callback; when merging the two, create only one frame):
if not SuperDPS.LosFrame then SuperDPS.LosSeen = {} local f = CreateFrame("Frame") f:RegisterEvent("UI_ERROR_MESSAGE") f:SetScript("OnEvent", function(_, _, _, msg) if msg == ERR_OUT_OF_RANGE or msg == SPELL_FAILED_LINE_OF_SIGHT then local g = UnitGUID("target") if g then SuperDPS.LosSeen[g] = GetTime() end end end) SuperDPS.LosFrame = fendProperty code:
local g = UnitGUID("target")if not g or not UnitIsVisible("target") then return 0 endlocal t = SuperDPS.LosSeen[g]if not t or GetTime() - t >= 1.5 then return 1 endreturn 04. Combo points, DK runes and pets
Section titled “4. Combo points, DK runes and pets”| Old property | Old Key | Old implementation | New version |
|---|---|---|---|
| Combo Points | PropC20A | GetComboPoints("player","target") |
📦 preset “Combo Points” (UnitPower(unit,4)) |
| DK runes available | PropC20B | GetRune() |
✅ Code 7 |
| DK rune slot cooldown | PropC20B2 | GetRuneByType2(slot) |
✅ Code 8 |
| DK Blood runes | PropC20C | GetRuneByType(1) |
❌ Retail has had no rune types since Legion; Classic clients still have types, but the old implementation relied on internal fields of Blizzard’s rune frame and must be rewritten and verified by you |
| DK Frost runes | PropC20D | GetRuneByType(3) |
❌ Same as above |
| DK Unholy runes | PropC20E | GetRuneByType(2) |
❌ Same as above |
| DK Death runes | PropC20F | GetRuneByType(4) |
❌ Same as above |
| has summoned pet | PropC20G | hasPet() (HasPetUI) |
📦 preset “Has Pet”; to distinguish hunter pets use ✅ Code 9 |
Rune cooldowns risk being protected in combat (which is why no official preset was ported). The implementations below are sanitized and yield 0 when protected:
Code 7: Total available runes (parameters: none):
local n = 0for i = 1, 6 do local _, _, ready = GetRuneCooldown(i) if SuperDPS.SN(ready, "rune") == true then n = n + 1 endendreturn nCode 8: Rune slot cooldown remaining (parameter: 1~6):
local start, duration = GetRuneCooldown(arg1)start = SuperDPS.SN(start, "runecd")duration = SuperDPS.SN(duration, "runecd")if start == 0 or duration == 0 then return 0 endlocal remain = start + duration - GetTime()if remain > 0 then return remain endreturn 0Code 9: Has summoned pet (old three-state semantics) (parameters: none; 2 = hunter pet, 1 = other pet, 0 = none):
local hasUI, isHunterPet = HasPetUI()if hasUI and isHunterPet then return 2 endif hasUI then return 1 endreturn 05. Spells and items
Section titled “5. Spells and items”| Old property | Old Key | Old implementation | New version |
|---|---|---|---|
| ranged weapon speed | PropC51C | UnitRangedDamage("player")*100 |
❌ The attack-speed family returns protected values on Midnight (the same thing that killed the swing timer) |
| spell cooldown | PropAab3 | GetSpellTime(id) (start+duration−now) |
📦 preset “Spell Cooldown” / “Spell Ready” (the old formula hits protection in combat) |
| spell charge cooldown | PropAab4 | GetSpellTime2(id) |
✅ Code 10 |
| spell cast time | Prop9Bf3 | SkillCastingTime(id) (measured and cached) |
✅ Code 11 |
| time since cast | Prop9Bf4 | getSpellCastTime(id) (event tracking) |
✅ Code 12 |
| time since cast, per target | Prop9Bf5 | getSpellCastTime2(id) |
✅ Code 12 (toggle the commented line) |
| DoT tick timer, per target | Prop9Bf6 | getSpellCastDamage (already a debuff approximation after CLEU was blocked) |
📦 preset “Own Debuff Remaining” |
| spell charges | PropC74D | GetSpellCharges(id) |
📦 preset “Spell Charges” |
| spell cast count | Prop70Ef | GetSpellCount(id) |
✅ Code 13 |
| item count | Prop6F49 | GetItemCount(id) |
📦 preset “Item Count” |
| raid buff count | Prop1F0E | getRaidBuffCount(id) |
✅ Code 14 |
| raid buff count, own | Prop98F1 | getRaidBuffCountByPlayer(id) |
✅ Code 14, append ,1 to the parameters |
Code 10: Spell charge cooldown remaining (parameter: spell ID):
local _, _, start, duration = GetSpellCharges(arg1)start = SuperDPS.SN(start, "chargecd")duration = SuperDPS.SN(duration, "chargecd")if start == 0 or duration == 0 then return 0 endlocal remain = start + duration - GetTime()if remain > 0 then return remain endreturn 0Code 11: Spell cast time (parameter: spell ID; returns seconds. The old unit was milliseconds/10, i.e. centiseconds ×10, so convert old thresholds accordingly). Uses static spell data instead of measuring and caching:
local info = C_Spell.GetSpellInfo(arg1)if not info then return 0 endreturn SuperDPS.SN(info.castTime, "casttime") / 1000Code 12: Time since cast (parameter: spell ID; capped at 120, returns 120 if never cast, same as the old semantics). The old version relied on the addon’s main event frame; the new version has the module create its own. Note the key changed from spell name to spell ID.
Put this in the init code:
if not SuperDPS.CastLogFrame then SuperDPS.CastAt = {} SuperDPS.CastAtTarget = {} local f = CreateFrame("Frame") f:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED") f:SetScript("OnEvent", function(_, _, unit, _, spellID) if unit == "player" and spellID then SuperDPS.CastAt[spellID] = GetTime() SuperDPS.CastAtTarget[spellID .. "_" .. (UnitGUID("target") or "")] = GetTime() end end) SuperDPS.CastLogFrame = fendProperty code:
local t = SuperDPS.CastAt[arg1]-- Per-target version: use the next line instead (times the same spell separately for each target)-- local t = SuperDPS.CastAtTarget[arg1 .. "_" .. (UnitGUID("target") or "")]if not t then return 120 endreturn math.min(GetTime() - t, 120)Code 13: Spell cast count (parameter: spell ID; “how many more casts are available”, as with Kill Shot-style spells):
return C_Spell.GetSpellCastCount and C_Spell.GetSpellCastCount(arg1) or 0Code 14: Raid buff count (parameter 1: aura ID or name; parameter 2: set to 1 to count only auras cast by yourself):
local n = 0for i = 1, 40 do local u = "raid" .. i if UnitExists(u) and not UnitIsDeadOrGhost(u) and UnitIsPlayer(u) then local name, _, _, _, _, _, source = SuperDPS.GetUnitBuff(u, arg1) if name and (not arg2 or source == "player") then n = n + 1 end endendreturn n6. Auras (buffs/debuffs)
Section titled “6. Auras (buffs/debuffs)”The old version had 31 properties in this family, all combinations of “unit × buff/debuff × remaining time/stacks × own-cast only”. The new version covers every combination with 4 generic pieces of code plus parameters. Unit is player/pet/target/focus; aura is a spell ID (recommended, avoids language differences) or a name.
| Old property (by group) | Old Key | New version |
|---|---|---|
| own/pet buff remaining | Prop6Ea9 / Prop3417 | 📦 preset “Buff Remaining”, parameters player,ID / pet,ID |
| own buff stacks | PropC16A | 📦 preset “Buff Stacks” |
| own buff remaining/stacks, own-cast | Prop6364 / Prop182B | ✅ Code 15/16, append ,1 to the parameters |
| own debuff remaining/stacks | PropE369 / Prop1C38 | 📦 “Debuff Remaining” / “Debuff Stacks”, parameters player,ID |
| target buff remaining/stacks, ± own-cast | Prop19Ca / PropA5Bf / PropA577 / PropD67D | ✅ Code 15/16, unit target |
| focus buff remaining/stacks, ± own-cast | Prop19Ca1 / PropA5Bf1 / PropA5771 / PropD67D1 | ✅ Code 15/16, unit focus |
| target debuff remaining/stacks, ± own-cast | PropD645 / Prop3416 / PropA1D0 / Prop17E6 | 📦 “Debuff Remaining” “Debuff Stacks” “Own Debuff Remaining” or ✅ Code 17 |
| focus debuff remaining/stacks, ± own-cast | PropD6451 / Prop34161 / PropA1D01 / Prop17E61 | Same, unit focus |
| aura icon remaining/stacks, ± own-cast | Prop3C59 / PropB6D7 / Prop3769 / Prop1Ff1 (self) Prop8E29 / Prop4E73 / Prop02E7 / Prop33E7 (target) |
✅ Code 18 (full search over buffs + debuffs) |
Code 15: Buff remaining time (generic) (parameters: unit,aura[,1 for own-cast only]). Keeps the old semantics: permanent buffs and buffs that just expired return 1, absent returns 0. The preset “Buff Remaining” returns 0 for permanent buffs; old modules whose logic depends on this difference should use this version:
local name, _, _, _, _, expire, source = SuperDPS.GetUnitBuff(arg1, arg2)if not name or (arg3 and source ~= "player") then return 0 endexpire = SuperDPS.SN(expire, "bufftime", arg1)local remain = expire - GetTime()if remain < 0 then return 1 endreturn remainCode 16: Buff stacks (generic) (parameters: unit,aura[,1 for own-cast only]):
local name, _, count, _, _, _, source = SuperDPS.GetUnitBuff(arg1, arg2)if not name or (arg3 and source ~= "player") then return 0 endreturn count or 0Code 17: Debuff remaining/stacks (generic) (parameters: unit,aura[,1 for own-cast only]). Debuffs have no “expired → 1” clamp (historical behavior, deliberately different from the buff version):
local name, _, count, _, _, expire, source = SuperDPS.GetUnitDebuff(arg1, arg2)if not name or (arg3 and source ~= "player") then return 0 endexpire = SuperDPS.SN(expire, "debufftime", arg1)if expire == 0 then return 0 endreturn expire - GetTime()-- Stacks version: delete the three lines above and return count or 0 insteadCode 18: Aura icon remaining (full search over buffs + debuffs) (parameters: unit,aura[,1 for own-cast only]). The old “icon” family did not distinguish buffs from debuffs and searched everything by name or ID, which maps to the new GetUnitAura without a filter:
local name, _, count, _, _, expire, source = SuperDPS.GetUnitAura(arg1, arg2)if not name or (arg3 and source ~= "player") then return 0 endexpire = SuperDPS.SN(expire, "auratime", arg1)local remain = expire - GetTime()if remain < 0 then return 1 endreturn remain-- Stacks version: delete the four lines above and return count or 0 instead7. Nameplates
Section titled “7. Nameplates”| Old property | Old Key | Old implementation | New version |
|---|---|---|---|
| nameplates with own debuff | Prop17E7 | getNamePlateDebuffCountByPlayer(id) |
✅ Code 19 |
| nameplates within range | Prop17E8 | getNamePlateRangeCount(yards) |
✅ Code 20 (the distance API is risky) or 📦 “Enemy Count” (no range limit) |
Nameplate units are limited by the in-game “enemy nameplates” toggle (V key by default) and the nameplate display distance.
Code 19: Number of nameplates carrying my DoT (parameter: debuff ID; for AoE DoT-refresh checks):
local n = 0for i = 1, 40 do local u = "nameplate" .. i if UnitExists(u) then local name, _, _, _, _, _, source = SuperDPS.GetUnitDebuff(u, arg1) if name and source == "player" then n = n + 1 end endendreturn nCode 20: Number of nameplates within N yards (parameter: yards, e.g. 8). Relies on UnitDistanceSquared; availability for hostile units on Midnight depends on in-game testing, and units that cannot be read are not counted:
local n = 0local r2 = arg1 * arg1for i = 1, 40 do local u = "nameplate" .. i if UnitExists(u) then local ok, d2 = pcall(UnitDistanceSquared, u) if ok then d2 = SuperDPS.SN(d2, "nprange") if d2 > 0 and d2 <= r2 then n = n + 1 end end endendreturn n8. Casting
Section titled “8. Casting”| Old property | Old Key | Old implementation | New version |
|---|---|---|---|
| target cast code | PropF717 | CastingInfo("target") (ID%255) |
📦 preset “Target Cast” (full ID, no more %255) |
| focus cast code | PropF718 | CastingInfo("focus") |
Same, parameter focus |
| own cast code | Prop6C83 | CastingInfo("player") |
Same, parameter player |
| own cast progress | Prop7F39 | CastingTime("player") ((end−now)/100) |
📦 preset “Cast Remaining” (in seconds), parameter player |
| target cast progress | Prop7F38 | CastingTime("target") |
Same, parameter target |
The old “cast code” took the spell ID modulo 255 (single-byte channel limit); conditions in old modules written against the modulo value must be changed to the full spell ID. The old “cast progress” unit was 0.1 s (milliseconds/100); the preset returns seconds.
9. Party members
Section titled “9. Party members”| Old property | Old Key | Old implementation | New version |
|---|---|---|---|
| target in spell range | PropD9D4 | IsSpellInRange(id) |
📦 preset “In Spell Range” |
| party dispel type check | Prop67C6 | GetDeBuffType(type) |
✅ Code 21 |
| lowest-health member index | Prop642E | getDamagePlayer() |
✅ Code 22 |
| lowest-health member damage % | PropF457 | getDamagePlayerHealth() |
✅ Code 22 (switch the return value) |
| lowest-health index, with buff | PropC0C7 | getDamagePlayerBuff(id) |
✅ Code 22, mode hasBuff |
| lowest-health damage %, with buff | Prop2838 | getDamagePlayerHealthBuff(id) |
Same |
| lowest-health index, without buff | Prop9A11 | getDamagePlayerNoBuff(id) |
✅ Code 22, mode noBuff |
| lowest-health damage %, without buff | PropD82C | getDamagePlayerHealthNoBuff(id) |
Same |
| lowest-health index, without debuff | PropA684 | getDamagePlayerNoDeBuff(id) |
✅ Code 22, mode noDebuff |
| lowest-health damage %, without debuff | PropB53B | getDamagePlayerHealthNoDeBuff(id) |
Same |
Index convention (same as the old version, used on the desktop side to pick a macro target by index): 1 = yourself, 24, 5 = party1640. Solo counts only yourself, a party counts yourself + party, a raid counts only raid units.45 = raid1
Code 21: Party dispel type check (parameter: Curse/Disease/Magic/Poison; returns the index of the first affected member, or 0 if nobody is affected):
local function hit(unit) if UnitIsDeadOrGhost(unit) or not UnitIsPlayer(unit) then return false end for j = 1, 40 do local name, _, _, dispelType = UnitDebuff(unit, j) if not name then return false end if dispelType == arg1 then return true end end return falseendif UnitPlayerOrPetInRaid("player") then for i = 1, 40 do local u = "raid" .. i if UnitExists(u) and hit(u) then return i + 5 end endelseif UnitPlayerOrPetInParty("player") then if hit("player") then return 1 end for i = 1, 4 do local u = "party" .. i if UnitExists(u) and hit(u) then return i + 1 end endelseif hit("player") then return 1endreturn 0Code 22: Lowest-health party member (whole family unified) (parameter 1: mode, empty/hasBuff/noBuff/noDebuff; parameter 2: aura ID, may be omitted when mode is empty). All 8 old properties share one scan, organized as “shared function defined in init code + thin call in the property row”, so multiple rows reuse it with zero duplication. Important on Midnight: health percent is sanitized, so members whose value is protected in combat are treated as full health and skipped; in heavy combat this whole family may degrade. The official presets deliberately did not port it, so healing modules should rely on it with care. The old version also applied a line-of-sight filter (error events); if you need it, add a section based on the LosSeen table from Code 6. One more semantic tweak: the old damage baseline was -1 (with everyone at full health it still picked the first qualifying member); this version’s baseline is 0 (returns 0 when nobody is damaged).
Put this in the init code:
-- Lowest-health group member scan, shared by several property rows. Returns: index, missing health percent (0~100)function SuperDPS.ScanLowHp(mode, aura) local best, bestDamage = 0, 0 local function consider(idx, unit) if not UnitExists(unit) or UnitIsDeadOrGhost(unit) or not UnitIsPlayer(unit) then return end if mode == "hasBuff" or mode == "noBuff" then local name = SuperDPS.GetUnitBuff(unit, aura) if (mode == "hasBuff") ~= (name ~= nil) then return end elseif mode == "noDebuff" then if SuperDPS.GetUnitDebuff(unit, aura) then return end end local g = UnitGUID(unit) if not g then return end local hp = SuperDPS.SN(UnitPercentHealthFromGUID(g), "lowhp", unit) if hp == 0 then return end -- dead / unreadable / protected → skip local damage = (1 - hp) * 100 if damage > bestDamage then best, bestDamage = idx, damage end end if UnitPlayerOrPetInRaid("player") then for i = 1, 40 do consider(i + 5, "raid" .. i) end else consider(1, "player") if UnitPlayerOrPetInParty("player") then for i = 1, 4 do consider(i + 1, "party" .. i) end end end return best, bestDamageendProperty code (“index” version):
local idx = SuperDPS.ScanLowHp(arg1, arg2)return idxProperty code (“damage %” version):
local _, damage = SuperDPS.ScanLowHp(arg1, arg2)return damage10. Weapons
Section titled “10. Weapons”| Old property | Old Key | Old implementation | New version |
|---|---|---|---|
| main-hand weapon CD | Prop9F61 | getSwingTimer("main") |
❌ UnitAttackSpeed returns a protected value on Midnight, so swing timing cannot be implemented (confirmed in-game; the whole subsystem was removed for this reason) |
| main-hand weapon CD, positive | Prop9F62 | getSwingTimer2("main") |
❌ Same as above |
| off-hand weapon CD | Prop72B3 | getSwingTimer("off") |
❌ Same as above |
| ranged weapon CD | Prop072B | getSwingTimer("ranged") |
❌ Same as above |
| main-hand temporary enchant ID | Prop66F0 | GetWeaponEnchantInfoMain() (%255) |
✅ Code 23 |
| off-hand temporary enchant ID | Prop093F | GetWeaponEnchantInfoOff() |
✅ Code 23 |
Code 23: Weapon temporary enchant ID (parameter: 4 main hand / 8 off hand; full ID, no more %255, so conditions in old modules written against the modulo value must be changed to the full enchant ID):
local id = select(arg1, GetWeaponEnchantInfo())return id or 011. UI, map and other
Section titled “11. UI, map and other”| Old property | Old Key | Old implementation | New version |
|---|---|---|---|
| button glow check | Prop44F6 | checkButtonOverlay(button) |
✅ Code 24 |
| UI visibility check | Prop44F7 | UiCheck(frame) |
✅ Code 25 |
| current map ID | Prop44F8 | MapId() (%255) |
✅ Code 26 |
| raid total damage % | Prop03Af | getAllHealth() (average damage %) |
✅ Code 27 (same degradation note as Code 22) |
| spell power | Prop0311 | GetSpellPower(divisor) |
✅ Code 28 |
Code 24: Button glow check (parameter: ActionButton1~ActionButton12). The old version checked the overlay field; the spell activation glow framework in newer clients renamed the field, so both are checked:
local b = _G[arg1]if b and (b.overlay or b.SpellActivationAlert) then return 1 endreturn 0Code 25: UI visibility check (parameter: the frame’s global name, e.g. PVPFrame):
local f = _G[arg1]if f and f.IsVisible and f:IsVisible() then return 1 endreturn 0Code 26: Current map ID (parameters: none; full ID, no more %255):
return C_Map.GetBestMapForUnit("player") or 0Code 27: Raid average damage percent (parameters: none; 0~100):
local total, n = 0, 0local function acc(unit) if not UnitExists(unit) or UnitIsDeadOrGhost(unit) then return end local g = UnitGUID(unit) if not g then return end local hp = SuperDPS.SN(UnitPercentHealthFromGUID(g), "allhp", unit) if hp == 0 then return end total = total + (1 - hp) n = n + 1endif UnitPlayerOrPetInRaid("player") then for i = 1, 40 do acc("raid" .. i) endelse acc("player") if UnitPlayerOrPetInParty("player") then for i = 1, 4 do acc("party" .. i) end endendif n == 0 then return 0 endreturn total / n * 100Code 28: Spell power (parameter: divisor. The old version used it to squeeze large values into the single-byte channel; in the new version fill 1 to get the raw value). Stat values may be protected in combat and become 0 after sanitizing:
local v = SuperDPS.SN(GetSpellBonusDamage(7), "spellpower")return v / (arg1 or 1)What cannot be ported (summary)
Section titled “What cannot be ported (summary)”| Old property | Reason |
|---|---|
| The whole swing timer family (main-hand/off-hand/ranged CD, ranged weapon speed) | UnitAttackSpeed/UnitRangedDamage return protected values on Midnight and throw on comparison (confirmed in-game 2026-08; the old addon’s swing subsystem was removed entirely because of this) |
| DK typed runes (Blood/Frost/Unholy/Death) | Retail has had no rune types since Legion, so this only makes sense on Classic; the old implementation also relied on the internal field _G["Rune"..i].rune.runeType of Blizzard’s rune frame (not a public API, breaks easily between versions). To use it, rewrite it yourself and verify on the target client |
| CLEU-based DoT tick timing | Combat log events are hard-blocked for addons (the late old version had already degraded to a debuff-presence approximation; use the preset “Own Debuff Remaining” directly) |
Degraded-but-usable list (may be unreadable in combat, treated as 0)
Section titled “Degraded-but-usable list (may be unreadable in combat, treated as 0)”- Percent types (codes 2/4/22/27): division/comparison must be sanitized, protected values become 0;
- Rune types (codes 7/8): cooldown values risk being protected;
- Range types (codes 5/20): availability of
UnitDistanceSquaredfor hostile units depends on in-game testing;