Files
pfUI/libs/librange.lua
T
Meow fa7e4f8b40 Range Check: UnitXP mode
Added Range Check Mode dropdown (Vanilla / UnitXP) and UnitXP Range Threshold input field under the 40y-Range Check settings in the GUI
In UnitXP mode, UnitInRange bypasses librange entirely and queries UnitXP("distanceBetween") directly with the configured yard threshold
librange scan loop is disabled when UnitXP mode is active — no TargetUnit cycling, no spellbook scanning, no combat interruption
Fixed UnitInRange referencing librange as an undefined local instead of pfUI.api.librange, which caused silent nil returns
2026-03-04 02:27:11 +01:00

319 lines
9.1 KiB
Lua

-- load pfUI environment
setfenv(1, pfUI:GetEnvironment())
--[[ librange ]]--
-- A pfUI library that detects and caches distance to units.
--
-- librange:UnitInSpellRange(unit)
-- Returns `1` if the unit is within a 40y range
--
-- return instantly when another librange is already active
if pfUI.api.librange then return end
local _, class = UnitClass("player")
local librange = CreateFrame("Frame", "pfRangecheck", UIParent)
-- table of 40y spells per class
local spells = {
["PALADIN"] = {
"Interface\\Icons\\Spell_Holy_FlashHeal",
"Interface\\Icons\\Spell_Holy_HolyBolt",
},
["PRIEST"] = {
"Interface\\Icons\\Spell_Holy_FlashHeal",
"Interface\\Icons\\Spell_Holy_LesserHeal",
"Interface\\Icons\\Spell_Holy_Heal",
"Interface\\Icons\\Spell_Holy_GreaterHeal",
"Interface\\Icons\\Spell_Holy_Renew",
},
["DRUID"] = {
"Interface\\Icons\\Spell_Nature_HealingTouch",
"Interface\\Icons\\Spell_Nature_ResistNature",
"Interface\\Icons\\Spell_Nature_Rejuvenation",
},
["SHAMAN"] = {
"Interface\\Icons\\Spell_Nature_MagicImmunity",
"Interface\\Icons\\Spell_Nature_HealingWaveLesser",
"Interface\\Icons\\Spell_Nature_HealingWaveGreater",
},
}
-- Use Nampower's IsSpellInRange if available (vanilla only)
-- This provides more accurate range checking without needing to find spell slots
local nampower_spell
if GetNampowerVersion then
librange:RegisterEvent("LEARNED_SPELL_IN_TAB")
librange:RegisterEvent("PLAYER_ENTERING_WORLD")
librange:SetScript("OnEvent", function()
-- abort on non healing classes
if not spells[class] then return end
nampower_spell = nil
for i = 1, GetNumSpellTabs() do
local _, _, offset, num = GetSpellTabInfo(i)
for id = offset + 1, offset + num do
local name, rank = GetSpellName(id, BOOKTYPE_SPELL)
local texture = GetSpellTexture(id, BOOKTYPE_SPELL)
if texture then
for _, tex in pairs(spells[class]) do
if tex == texture then
nampower_spell = name
end
end
end
end
end
end)
function librange:UnitInSpellRange(unit)
if not nampower_spell then return nil end
-- Nampower's IsSpellInRange returns 1 if in range, 0 if not, -1 if invalid
local result = IsSpellInRange(nampower_spell, unit)
if result == 1 then return 1
elseif result == 0 then return nil
else return nil end
end
-- add librange to pfUI API
pfUI.api.librange = librange
return
end
-- units that should be scanned
local units = {}
table.insert(units, "pet")
for i=1,4 do table.insert(units, "party" .. i) end
for i=1,4 do table.insert(units, "partypet" .. i) end
for i=1,40 do table.insert(units, "raid" .. i) end
for i=1,40 do table.insert(units, "raidpet" .. i) end
local numunits = table.getn(units)
-- cache for unit relations
local unitcache = {}
-- actual unit-range table
local unitdata = { }
-- setup sound function switches
local SoundOn = PlaySound
local SoundOff = function() return end
librange.id = 1
-- Shooting with wands does not make the PlayerFrame inCombat attribute change.
-- This frame makes wand attacks accesible via wandCombat on the PlayerFrame.
local wand = CreateFrame("Frame", "pfWandShootDetect")
wand:RegisterEvent("START_AUTOREPEAT_SPELL")
wand:RegisterEvent("STOP_AUTOREPEAT_SPELL")
wand:SetScript("OnEvent", function()
PlayerFrame.wandCombat = event == "START_AUTOREPEAT_SPELL" and true or nil
end)
--Players with combo points aren't necessarily auto attacking, meaning we can't use inCombat.
--This allows us to avoid rangechecking when the player has combo points to avoid losing them.
local hascombopoints
local combo = CreateFrame("Frame", "pfComboPointsDetect")
combo:RegisterEvent("PLAYER_COMBO_POINTS")
combo:SetScript("OnEvent", function()
hascombopoints = GetComboPoints() > 0
end)
-- Flag to prevent UnitXP calls during logout (crash prevention)
local librange_isLoggingOut = false
librange:Hide()
librange:RegisterEvent("ACTIONBAR_SLOT_CHANGED")
librange:RegisterEvent("PLAYER_ENTERING_WORLD")
librange:RegisterEvent("PLAYER_ENTER_COMBAT")
librange:RegisterEvent("PLAYER_LEAVE_COMBAT")
librange:RegisterEvent("PLAYER_LOGOUT")
librange:RegisterEvent("PLAYER_LEAVING_WORLD")
librange:SetScript("OnEvent", function()
-- Handle logout to prevent UnitXP crashes during shutdown
if event == "PLAYER_LOGOUT" or event == "PLAYER_LEAVING_WORLD" then
librange_isLoggingOut = true
this:SetScript("OnUpdate", nil) -- Stop OnUpdate completely
this:Hide()
return
end
-- disable range checking activities
if pfUI_config.unitframes.rangecheck == "0" or not spells[class] then
this:Hide()
return
end
this.interval = tonumber(C.unitframes.rangechecki)/numunits
if event == "ACTIONBAR_SLOT_CHANGED" or event == "PLAYER_ENTERING_WORLD" then
librange.slot = this:GetRangeSlot()
this:Show()
elseif event == "PLAYER_ENTER_COMBAT" then
this.lastattack = GetTime()
this:Hide()
elseif event == "PLAYER_LEAVE_COMBAT" then
if not this:ReAttack() then
this:Show()
end
end
end)
local _, class = UnitClass("player")
local druid = class == "DRUID"
local target_event = TargetFrame_OnEvent
local target_nop = function() return end
librange:SetScript("OnUpdate", function()
-- Prevent UnitXP calls during logout (crash prevention)
if librange_isLoggingOut then return end
if ( this.tick or 1) > GetTime() then
return
else
this.tick = GetTime() + this.interval
end
-- skip invalid units
while not this:NeedRangeScan(units[this.id]) and this.id <= numunits do
this.id = this.id + 1
end
if this.id <= numunits and librange.slot then
local unit = units[this.id]
if not UnitIsUnit("target", unit) then
-- Try UnitXP_SP3 first (most accurate distance measurement)
local unitxp_success, unitxp_distance = pcall(function()
return UnitXP("distanceBetween", "player", unit)
end)
if unitxp_success and unitxp_distance then
local threshold = (tonumber(C.unitframes.rangecheck_distance) or 40) + 5
unitdata[unit] = unitxp_distance < threshold and 1 or 0
this.id = this.id + 1
return
end
-- try to read distance via superwow second
if HasSuperWoW() and UnitPosition then
local x1, y1, z1 = UnitPosition("player")
local x2, y2, z2 = UnitPosition(unit)
-- only continue if we got position values
if x1 and y1 and z1 and x2 and y2 and z2 then
local distance = ((x2 - x1)^2 + (y2 - y1)^2 + (z2 - z1)^2)^.5
unitdata[unit] = distance < 45 and 1 or 0
this.id = this.id + 1
return
end
end
-- suspend for various conditions
if pfUI.loot and pfUI.loot:IsShown() then return nil end
if LootFrame and LootFrame:IsShown() then return nil end
if InspectFrame and InspectFrame:IsShown() then return nil end
if TradeFrame and TradeFrame:IsShown() then return nil end
if PlayerFrame and PlayerFrame.inCombat then return nil end
if PlayerFrame and PlayerFrame.wandCombat then return nil end
if druid and UnitPowerType("player") == 3 then return nil end
if hascombopoints then return nil end
_G.PlaySound = SoundOff
pfScanActive = true
-- save and disable target frame events
target_event = TargetFrame_OnEvent
_G.TargetFrame_OnEvent = target_nop
TargetUnit(unit)
unitdata[unit] = IsActionInRange(librange.slot)
TargetLastTarget()
-- restore target events
_G.TargetFrame_OnEvent = target_event
_G.PlaySound = SoundOn
pfScanActive = false
this:ReAttack()
end
this.id = this.id + 1
else
this.id = 1
end
end)
function librange:ReAttack()
-- we accidentally broke the autoattack... restoring the old state
if this.lastattack and this.lastattack + this.interval > GetTime() and UnitCanAttack("player", "target") then
AttackTarget()
return true
else
return nil
end
end
function librange:NeedRangeScan(unit)
if not UnitExists(unit) then return nil end
if not UnitIsVisible(unit) then return nil end
if CheckInteractDistance(unit, 4) then return nil end
return true
end
function librange:GetRealUnit(unit)
if unitdata[unit] then return unit end
if unitcache[unit] then
if UnitIsUnit(unitcache[unit], unit) then
return unitcache[unit]
end
end
for id, realunit in pairs(units) do
if UnitIsUnit(realunit, unit) then
unitcache[unit] = realunit
return realunit
end
end
return unit
end
function librange:GetRangeSlot()
local texture
for i=1,120 do
texture = GetActionTexture(i)
if texture and not GetActionText(i) then
for _, check in pairs(spells[class]) do
if check == texture then
return i
end
end
end
end
return nil
end
function librange:UnitInSpellRange(unit)
if not librange.slot then return nil end
if UnitIsUnit("target", unit) then
return IsActionInRange(librange.slot) == 1 and 1 or nil
end
local unit = librange:GetRealUnit(unit)
if unitdata[unit] and unitdata[unit] == 1 then
return 1
elseif not unitdata[unit] then
return 1
else
return nil
end
end
-- add librange to pfUI API
pfUI.api.librange = librange