---@class DBM local DBM = DBM ---@class DBMRangeCheck local rangeCheck = {} DBM.RangeCheck = rangeCheck -------------- -- Locals -- -------------- local isRetail = WOW_PROJECT_ID == (WOW_PROJECT_MAINLINE or 1) local isWrath = WOW_PROJECT_ID == (WOW_PROJECT_WRATH_CLASSIC or 11) local isClassic = WOW_PROJECT_ID == (WOW_PROJECT_CLASSIC or 2) local DDM = _G["LibStub"]:GetLibrary("LibDropDownMenu") local UIDropDownMenu_AddButton, UIDropDownMenu_Initialize, ToggleDropDownMenu = DDM.UIDropDownMenu_AddButton, DDM.UIDropDownMenu_Initialize, DDM.ToggleDropDownMenu local function UnitPhaseReasonHack(uId) if isRetail then return not UnitPhaseReason(uId) end return UnitInPhase(uId) end local L = DBM_CORE_L ---@class DBMRangeCheckFrame: Frame local mainFrame = CreateFrame("Frame") local textFrame, radarFrame, updateIcon, updateRangeFrame, initializeDropdown local RAID_CLASS_COLORS = _G["CUSTOM_CLASS_COLORS"] or RAID_CLASS_COLORS -- For Phanx' Class Colors -- Function for automatically converting inputed ranges from old mods to be ones that have valid item/api checks local function setCompatibleRestrictedRange(range) if range <= 4 and isRetail then return 4 elseif range <= 8 then return 8 elseif range <= 10 then return 10 elseif range <= 11 then return 11 elseif range <= 13 then return 13 elseif range <= 18 then return 18 elseif range <= 23 then return 23 elseif range <= 28 then return 28 elseif range <= 33 then return 33 elseif range <= 43 then return 43 elseif range <= 48 and not isClassic then return 48 elseif range <= 60 and not isClassic then return 60 elseif range <= 80 and not isClassic then return 80 elseif range <= 100 and not isClassic then return 100 else--Mod passed a range that exceeds max range known apis can cover, we really don't have a way to measure this anymore so we return highest range we can measure based on game client return isClassic and 43 or 100 end end ----------------------- -- Check functions -- ----------------------- local getDistanceBetween, getDistanceBetweenAll local itsBCAgain--Needs to be called outside of below scope, itsDFBaby does not do local UnitPosition, UnitExists, UnitIsUnit, UnitIsDeadOrGhost, UnitIsConnected = UnitPosition, UnitExists, UnitIsUnit, UnitIsDeadOrGhost, UnitIsConnected local CheckInteractDistance, IsItemInRange, UnitInRange = CheckInteractDistance, C_Item and C_Item.IsItemInRange or IsItemInRange, UnitInRange -- All ranges are tested and compared against UnitDistanceSquared. -- Example: Worgsaw has a tooltip of 6 but doesn't factor in hitboxes/etc. It doesn't return false until UnitDistanceSquared of 8. local itemRanges = { [8] = 8149, -- Voodoo Charm [13] = 17626, -- Sparrowhawk Net [18] = 6450, -- Silk Bandage [23] = 21519, -- Mistletoe [28] = 13289,--Egan's Blaster [33] = 1180, -- Scroll of Stamina } if not isClassic then -- Exists in Wrath/BCC but not vanilla/era itemRanges[6] = 16114 -- Foremans Blackjack (TBC) itemRanges[43] = 34471 -- Vial of the Sunwell (UnitInRange api alternate if item checks break) itemRanges[48] = 32698 -- Wrangling Rope itemRanges[60] = 32825 -- Soul Cannon itemRanges[80] = 35278 -- Reinforced Net (WotLK) itemRanges[100] = 41058 -- Hyldnir Harpoon (WotLK) end local apiRanges = { [10] = 3, -- CheckInteractDistance (Duel) [11] = 2, -- CheckInteractDistance (Trade) } local function itsDFBaby(uId) local inRange, checkedRange = UnitInRange(uId) if inRange and checkedRange then--Range checked and api was successful return 43 else return 1000 end end function itsBCAgain(uId, checkrange) if checkrange then -- Specified range, this check only cares whether unit is within specific range if not isRetail and checkrange == 43 then -- Only classic/BCC uses UnitInRange so only classic has this check, TBC+ can use Vial of the Sunwell return UnitInRange(uId) and checkrange or 1000 elseif itemRanges[checkrange] then -- Only query item range for requested active range check return IsItemInRange(itemRanges[checkrange], uId) and checkrange or 1000 elseif apiRanges[checkrange] then -- Only query item range for requested active range if no item found for it return CheckInteractDistance(uId, apiRanges[checkrange]) and checkrange or 1000 else return 1000 -- Just so it has a numeric value, even if it's unknown to protect from nil errors end else -- No range passed, this is being used by a getDistanceBetween function that needs to calculate precise distances of members of raid (well as precise as possible with a crappy api) if isRetail and IsItemInRange(90175, uId) then return 4 elseif not isClassic and IsItemInRange(16114, uId) then return 6 elseif IsItemInRange(8149, uId) then return 8 elseif CheckInteractDistance(uId, 3) then return 10 elseif CheckInteractDistance(uId, 2) then return 11 elseif IsItemInRange(isClassic and 17626 or 32321 , uId) then return 13 elseif IsItemInRange(6450, uId) then return 18 elseif IsItemInRange(21519, uId) then return 23 elseif IsItemInRange(13289, uId) then return 28 elseif IsItemInRange(1180, uId) then return 33 elseif UnitInRange and UnitInRange(uId) then return 43 elseif not isClassic and IsItemInRange(32698, uId) then return 48 elseif not isClassic and IsItemInRange(32825, uId) then return 60 elseif not isClassic and IsItemInRange(35278, uId) then return 80 elseif not isClassic and IsItemInRange(41058, uId) then return 100 else return 1000 end -- Just so it has a numeric value, even if it's unknown to protect from nil errors end end --Retail is limited to just returning true or false for being within 43 (40+hitbox) of target while in instances (outdoors retail can still use UnitDistanceSquared) function getDistanceBetweenAll(checkrange) local restrictionsActive = not isWrath and DBM:HasMapRestrictions() checkrange = restrictionsActive and 43 or checkrange for uId in DBM:GetGroupMembers() do if UnitExists(uId) and not UnitIsUnit(uId, "player") and not UnitIsDeadOrGhost(uId) and UnitIsConnected(uId) and UnitPhaseReasonHack(uId) then local range = DBM:HasMapRestrictions() and (not isWrath and itsDFBaby(uId) or itsBCAgain(uId, checkrange)) or UnitDistanceSquared(uId) * 0.5 if checkrange < (range + 0.5) then return true end end end return false end function getDistanceBetween(uId, x, y) local restrictionsActive = DBM:HasMapRestrictions() if not x then -- If only one arg then 2nd arg is always assumed to be player return restrictionsActive and (not isWrath and itsDFBaby(uId) or itsBCAgain(uId)) or UnitDistanceSquared(uId) ^ 0.5 end if type(x) == "string" and UnitExists(x) then -- arguments: uId, uId2 -- First attempt to avoid UnitPosition if any of args is player UnitDistanceSquared should work if UnitIsUnit("player", uId) then return restrictionsActive and (not isWrath and itsDFBaby(x) or itsBCAgain(x)) or UnitDistanceSquared(x) ^ 0.5 elseif UnitIsUnit("player", x) then return restrictionsActive and (not isWrath and itsDFBaby(uId) or itsBCAgain(uId)) or UnitDistanceSquared(uId) ^ 0.5 else -- Neither unit is player, no way to avoid UnitPosition if restrictionsActive then -- Cannot compare two units that don't involve player with restrictions, just fail quietly return 1000 end local uId2 = x x, y = UnitPosition(uId2) if not x then print("getDistanceBetween failed for: " .. uId .. " (" .. tostring(UnitExists(uId)) .. ") and " .. uId2 .. " (" .. tostring(UnitExists(uId2)) .. ")") return end end end if restrictionsActive then -- Cannot check distance between player and a location (not another unit, again, fail quietly) return 1000 end local startX, startY = UnitPosition(uId) local dX = startX - x local dY = startY - y return (dX * dX + dY * dY) ^ 0.5 end end --------------------- -- Dropdown Menu -- --------------------- do local sound0 = "none" local sound1 = "Interface\\AddOns\\DBM-Core\\Sounds\\blip_8.ogg" local sound2 = "Interface\\AddOns\\DBM-Core\\Sounds\\alarmclockbeeps.ogg" local function setSound(self, option, sound) DBM.Options[option] = sound if sound ~= "none" then DBM:PlaySoundFile(sound) end end local function setRange(self, range) rangeCheck:Hide(true) rangeCheck:Show(range, mainFrame.filter, true, mainFrame.redCircleNumPlayers or 1) end local function setThreshold(self, threshold) rangeCheck:Hide(true) rangeCheck:Show(mainFrame.range, mainFrame.filter, true, threshold) end local function setFrames(self, option) DBM.Options.RangeFrameFrames = option rangeCheck:Hide(true) rangeCheck:Show(mainFrame.range, mainFrame.filter, true, mainFrame.redCircleNumPlayers or 1) end local function toggleLocked() DBM.Options.RangeFrameLocked = not DBM.Options.RangeFrameLocked end function initializeDropdown(_, level, menu) local info if level == 1 then info = {} info.text = L.RANGECHECK_SETRANGE info.notCheckable = true info.hasArrow = true info.keepShownOnClick = true info.menuList = "range" UIDropDownMenu_AddButton(info, 1) info = {} info.text = L.RANGECHECK_SETTHRESHOLD info.notCheckable = true info.hasArrow = true info.keepShownOnClick = true info.menuList = "threshold" UIDropDownMenu_AddButton(info, 1) info = {} info.text = L.RANGECHECK_SOUNDS info.notCheckable = true info.hasArrow = true info.keepShownOnClick = true info.menuList = "sounds" UIDropDownMenu_AddButton(info, 1) info = {} info.text = L.RANGECHECK_OPTION_FRAMES info.notCheckable = true info.hasArrow = true info.keepShownOnClick = true info.menuList = "frames" UIDropDownMenu_AddButton(info, 1) info = {} info.text = LOCK_FRAME if DBM.Options.RangeFrameLocked then info.checked = true end info.func = toggleLocked UIDropDownMenu_AddButton(info, 1) info = {} info.text = HIDE info.notCheckable = true info.func = function() rangeCheck:Hide(true) end info.arg1 = rangeCheck UIDropDownMenu_AddButton(info, 1) elseif level == 2 then if menu == "range" then if not isClassic then info = {} info.text = L.RANGECHECK_SETRANGE_TO:format(6) info.func = setRange info.arg1 = 6 info.checked = (mainFrame.range == 6) UIDropDownMenu_AddButton(info, 2) end info = {} info.text = L.RANGECHECK_SETRANGE_TO:format(8) info.func = setRange info.arg1 = 8 info.checked = (mainFrame.range == 8) UIDropDownMenu_AddButton(info, 2) info = {} info.text = L.RANGECHECK_SETRANGE_TO:format(10) info.func = setRange info.arg1 = 10 info.checked = (mainFrame.range == 10) UIDropDownMenu_AddButton(info, 2) info = {} info.text = L.RANGECHECK_SETRANGE_TO:format(13) info.func = setRange info.arg1 = 13 info.checked = (mainFrame.range == 13) UIDropDownMenu_AddButton(info, 2) info = {} info.text = L.RANGECHECK_SETRANGE_TO:format(18) info.func = setRange info.arg1 = 18 info.checked = (mainFrame.range == 18) UIDropDownMenu_AddButton(info, 2) info = {} info.text = L.RANGECHECK_SETRANGE_TO:format(23) info.func = setRange info.arg1 = 23 info.checked = (mainFrame.range == 23) UIDropDownMenu_AddButton(info, 2) info = {} info.text = L.RANGECHECK_SETRANGE_TO:format(33) info.func = setRange info.arg1 = 33 info.checked = (mainFrame.range == 33) UIDropDownMenu_AddButton(info, 2) if not isClassic then info = {} info.text = L.RANGECHECK_SETRANGE_TO:format(43) info.func = setRange info.arg1 = 43 info.checked = (mainFrame.range == 43) UIDropDownMenu_AddButton(info, 2) end elseif menu == "threshold" then info = {} info.text = 1 info.func = setThreshold info.arg1 = 1 info.checked = (mainFrame.redCircleNumPlayers == 1) UIDropDownMenu_AddButton(info, 2) info = {} info.text = 2 info.func = setThreshold info.arg1 = 2 info.checked = (mainFrame.redCircleNumPlayers == 2) UIDropDownMenu_AddButton(info, 2) info = {} info.text = 3 info.func = setThreshold info.arg1 = 3 info.checked = (mainFrame.redCircleNumPlayers == 3) UIDropDownMenu_AddButton(info, 2) info = {} info.text = 4 info.func = setThreshold info.arg1 = 4 info.checked = (mainFrame.redCircleNumPlayers == 4) UIDropDownMenu_AddButton(info, 2) info = {} info.text = 5 info.func = setThreshold info.arg1 = 5 info.checked = (mainFrame.redCircleNumPlayers == 5) UIDropDownMenu_AddButton(info, 2) info = {} info.text = 6 info.func = setThreshold info.arg1 = 6 info.checked = (mainFrame.redCircleNumPlayers == 6) UIDropDownMenu_AddButton(info, 2) info = {} info.text = 8 info.func = setThreshold info.arg1 = 8 info.checked = (mainFrame.redCircleNumPlayers == 8) UIDropDownMenu_AddButton(info, 2) elseif menu == "sounds" then info = {} info.text = L.RANGECHECK_SOUND_OPTION_1 info.notCheckable = true info.hasArrow = true info.menuList = "RangeFrameSound1" UIDropDownMenu_AddButton(info, 2) info = {} info.text = L.RANGECHECK_SOUND_OPTION_2 info.notCheckable = true info.hasArrow = true info.menuList = "RangeFrameSound2" UIDropDownMenu_AddButton(info, 2) elseif menu == "frames" then info = {} info.text = L.RANGECHECK_OPTION_TEXT info.func = setFrames info.arg1 = "text" info.checked = (DBM.Options.RangeFrameFrames == "text") UIDropDownMenu_AddButton(info, 2) info = {} info.text = L.RANGECHECK_OPTION_RADAR info.func = setFrames info.arg1 = "radar" info.checked = (DBM.Options.RangeFrameFrames == "radar") UIDropDownMenu_AddButton(info, 2) info = {} info.text = L.RANGECHECK_OPTION_BOTH info.func = setFrames info.arg1 = "both" info.checked = (DBM.Options.RangeFrameFrames == "both") UIDropDownMenu_AddButton(info, 2) end elseif level == 3 then local option = menu info = {} info.text = L.RANGECHECK_SOUND_0 info.func = setSound info.arg1 = option info.arg2 = sound0 info.checked = (DBM.Options[option] == sound0) UIDropDownMenu_AddButton(info, 3) info = {} info.text = L.RANGECHECK_SOUND_1 info.func = setSound info.arg1 = option info.arg2 = sound1 info.checked = (DBM.Options[option] == sound1) UIDropDownMenu_AddButton(info, 3) info = {} info.text = L.RANGECHECK_SOUND_2 info.func = setSound info.arg1 = option info.arg2 = sound2 info.checked = (DBM.Options[option] == sound2) UIDropDownMenu_AddButton(info, 3) end end end ----------------- -- Play Sounds -- ----------------- local updateSound local soundUpdate = 0 do local UnitAffectingCombat = UnitAffectingCombat function updateSound(num) if not UnitAffectingCombat("player") or (GetTime() - soundUpdate) < 5 then return end soundUpdate = GetTime() if num == 1 then if DBM.Options.RangeFrameSound1 ~= "none" then DBM:PlaySoundFile(DBM.Options.RangeFrameSound1) end elseif num > 1 then if DBM.Options.RangeFrameSound2 ~= "none" then DBM:PlaySoundFile(DBM.Options.RangeFrameSound2) end end end end ------------------------ -- Create the frame -- ------------------------ local function createTextFrame() ---@class DBMRangeCheckFrame: Frame, BackdropTemplate textFrame = CreateFrame("Frame", "DBMRangeCheck", UIParent, "BackdropTemplate") textFrame:SetFrameStrata("DIALOG") textFrame.backdropInfo = { bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",--131071 tile = true, tileSize = 16 } textFrame:ApplyBackdrop() textFrame:SetPoint(DBM.Options.RangeFramePoint, UIParent, DBM.Options.RangeFramePoint, DBM.Options.RangeFrameX, DBM.Options.RangeFrameY) textFrame:SetSize(128, 12) textFrame:SetClampedToScreen(true) textFrame:EnableMouse(true) textFrame:SetToplevel(true) textFrame:SetMovable(true) textFrame:RegisterForDrag("LeftButton") textFrame:SetScript("OnDragStart", function(self) if not DBM.Options.RangeFrameLocked then self:StartMoving() end end) textFrame:SetScript("OnDragStop", function(self) self:StopMovingOrSizing() local point, _, _, x, y = self:GetPoint(1) DBM.Options.RangeFrameX = x DBM.Options.RangeFrameY = y DBM.Options.RangeFramePoint = point end) textFrame:SetScript("OnMouseDown", function(_, button) if button == "RightButton" then local dropdownFrame = CreateFrame("Frame", "DBMRangeCheckDropdown", UIParent) ---@diagnostic disable-next-line: param-type-mismatch UIDropDownMenu_Initialize(dropdownFrame, initializeDropdown) ---@diagnostic disable-next-line: param-type-mismatch ToggleDropDownMenu(1, nil, dropdownFrame, "cursor", 5, -10) end end) ---@class DBMRangeCheckTitleFrame: FontString local text = textFrame:CreateFontString(nil, "OVERLAY", "GameTooltipText") text:SetSize(128, 15) text:SetPoint("BOTTOMLEFT", textFrame, "TOPLEFT") text:SetTextColor(1, 1, 1, 1) text:Show() text.OldSetText = text.SetText text.SetText = function(self, text) self:OldSetText(text) self:SetWidth(0) -- Set the text width to 0, so the system can auto-calculate the size end textFrame.text = text local inRangeText = textFrame:CreateFontString(nil, "OVERLAY", "GameTooltipText") inRangeText:SetSize(128, 15) inRangeText:SetPoint("TOPLEFT", textFrame, "BOTTOMLEFT") inRangeText:SetTextColor(1, 1, 1, 1) inRangeText:Hide() textFrame.inRangeText = inRangeText textFrame.lines = {} for i = 1, 5 do local line = textFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal") line:SetSize(128, 12) line:SetJustifyH("LEFT") if i == 1 then line:SetPoint("TOPLEFT", textFrame, "TOPLEFT", 6, -6) else line:SetPoint("TOPLEFT", textFrame.lines[i - 1], "LEFT", 0, -6) end textFrame.lines[i] = line end textFrame:Hide() end local function createRadarFrame() ---@class DBMRangeCheckRadarFrame: Frame radarFrame = CreateFrame("Frame", "DBMRangeCheckRadar", UIParent) radarFrame:SetFrameStrata("DIALOG") radarFrame:SetPoint(DBM.Options.RangeFrameRadarPoint, UIParent, DBM.Options.RangeFrameRadarPoint, DBM.Options.RangeFrameRadarX, DBM.Options.RangeFrameRadarY) radarFrame:SetSize(128, 128) radarFrame:SetClampedToScreen(true) radarFrame:EnableMouse(true) radarFrame:SetToplevel(true) radarFrame:SetMovable(true) radarFrame:RegisterForDrag("LeftButton") radarFrame:SetScript("OnDragStart", function(self) if not DBM.Options.RangeFrameLocked then self:StartMoving() end end) radarFrame:SetScript("OnDragStop", function(self) self:StopMovingOrSizing() local point, _, _, x, y = self:GetPoint(1) DBM.Options.RangeFrameRadarX = x DBM.Options.RangeFrameRadarY = y DBM.Options.RangeFrameRadarPoint = point end) radarFrame:SetScript("OnMouseDown", function(_, button) if button == "RightButton" then local dropdownFrame = CreateFrame("Frame", "DBMRangeCheckDropdown", UIParent) ---@diagnostic disable-next-line: param-type-mismatch UIDropDownMenu_Initialize(dropdownFrame, initializeDropdown) ---@diagnostic disable-next-line: param-type-mismatch ToggleDropDownMenu(1, nil, dropdownFrame, "cursor", 5, -10) end end) local bg = radarFrame:CreateTexture(nil, "BACKGROUND") bg:SetAllPoints(radarFrame) bg:SetBlendMode("BLEND") bg:SetColorTexture(0, 0, 0, 0.3) radarFrame.background = bg local circle = radarFrame:CreateTexture(nil, "ARTWORK") circle:SetSize(85, 85) circle:SetPoint("CENTER") circle:SetTexture("Interface\\AddOns\\DBM-Core\\textures\\radar_circle.blp") circle:SetVertexColor(0, 1, 0) circle:SetBlendMode("ADD") radarFrame.circle = circle local player = radarFrame:CreateTexture(nil, "OVERLAY") player:SetSize(32, 32) player:SetTexture(136431) -- "Interface\\Minimap\\MinimapArrow.blp" player:SetBlendMode("ADD") player:SetPoint("CENTER") local text = radarFrame:CreateFontString(nil, "OVERLAY", "GameTooltipText") text:SetSize(128, 15) text:SetPoint("BOTTOMLEFT", radarFrame, "TOPLEFT") text:SetTextColor(1, 1, 1, 1) text:Show() radarFrame.text = text local inRangeText = radarFrame:CreateFontString(nil, "OVERLAY", "GameTooltipText") inRangeText:SetSize(128, 15) inRangeText:SetPoint("TOPLEFT", radarFrame, "BOTTOMLEFT") inRangeText:SetTextColor(1, 1, 1, 1) inRangeText:Hide() radarFrame.inRangeText = inRangeText radarFrame.dots = {} for i = 1, 40 do local dot = radarFrame:CreateTexture(nil, "OVERLAY") dot:SetSize(24, 24) dot:SetTexture(249183) -- "Interface\\Minimap\\PartyRaidBlips" dot:Hide() radarFrame.dots[i] = dot end radarFrame:Hide() end ---------------- -- OnUpdate -- ---------------- do local UnitExists, UnitIsUnit, UnitIsDeadOrGhost, UnitIsConnected, GetPlayerFacing, UnitClass, IsInRaid, GetNumGroupMembers, GetRaidTargetIndex, GetBestMapForUnit = UnitExists, UnitIsUnit, UnitIsDeadOrGhost, UnitIsConnected, GetPlayerFacing, UnitClass, IsInRaid, GetNumGroupMembers, GetRaidTargetIndex, C_Map.GetBestMapForUnit local max, min, sin, cos, pi2 = math.max, math.min, math.sin, math.cos, math.pi * 2 local circleColor, rotation, pixelsperyard, activeDots, prevRange, prevThreshold, prevNumClosePlayer, prevclosestRange, prevColor, prevType = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 local unitList = {} local BLIP_TEX_COORDS = { ["WARRIOR"] = { 0, 0.125, 0, 0.25 }, ["PALADIN"] = { 0.125, 0.25, 0, 0.25 }, ["HUNTER"] = { 0.25, 0.375, 0, 0.25 }, ["ROGUE"] = { 0.375, 0.5, 0, 0.25 }, ["PRIEST"] = { 0.5, 0.625, 0, 0.25 }, ["DEATHKNIGHT"] = { 0.625, 0.75, 0, 0.25 }, ["SHAMAN"] = { 0.75, 0.875, 0, 0.25 }, ["MAGE"] = { 0.875, 1, 0, 0.25 }, ["WARLOCK"] = { 0, 0.125, 0.25, 0.5 }, ["DRUID"] = { 0.25, 0.375, 0.25, 0.5 }, ["MONK"] = { 0.125, 0.25, 0.25, 0.5 }, ["DEMONHUNTER"] = { 0.375, 0.5, 0.25, 0.5 }, ["EVOKER"] = { 0, 0.125, 0, 0.25 }, -- Uses the same as WARRIOR, because that's what Blizzard is doing currently } local function setDot(id, sinTheta, cosTheta) local dot = radarFrame.dots[id] if dot.range < (mainFrame.range * 1.5) then -- If person is closer than 1.5 * range, show the dot. Else hide it dot:ClearAllPoints() dot:SetPoint("CENTER", radarFrame, "CENTER", ((dot.x * cosTheta) - (-dot.y * sinTheta)) * pixelsperyard, ((dot.x * sinTheta) + (-dot.y * cosTheta)) * pixelsperyard) dot:Show() elseif dot:IsShown() then dot:Hide() end end function updateIcon() local numPlayers = GetNumGroupMembers() or 0 activeDots = max(numPlayers, activeDots) for i = 1, activeDots do local dot = radarFrame.dots[i] if i <= numPlayers then unitList[i] = IsInRaid() and "raid" .. i or "party" .. i local _, class = UnitClass(unitList[i]) local icon = GetRaidTargetIndex(unitList[i]) dot.class = class if icon and icon < 9 then dot.icon = icon dot:SetTexture(13700 .. icon) -- "Interface\\TargetingFrame\\UI-RaidTargetingIcon_" .. icon dot:SetTexCoord(0, 1, 0, 1) dot:SetSize(16, 16) dot:SetDrawLayer("OVERLAY", 1) else dot.icon = nil class = class or "PRIEST" dot:SetTexture(249183) -- "Interface\\Minimap\\PartyRaidBlips" dot:SetTexCoord(BLIP_TEX_COORDS[class][1], BLIP_TEX_COORDS[class][2], BLIP_TEX_COORDS[class][3], BLIP_TEX_COORDS[class][4]) dot:SetSize(24, 24) dot:SetDrawLayer("OVERLAY", 0) end elseif dot:IsShown() then dot:Hide() end end end function updateRangeFrame() if mainFrame.hideTime > 0 and GetTime() > mainFrame.hideTime then rangeCheck:Hide() return end local activeRange = mainFrame.range local restricted = mainFrame.restrictions local tEnabled = textFrame:IsShown() local rEnabled = radarFrame:IsShown() local reverse = mainFrame.reverse local warnThreshold = mainFrame.redCircleNumPlayers if tEnabled then for i = 1, 5 do textFrame.lines[i]:SetText("") textFrame.lines[i]:Hide() end if reverse then if warnThreshold > 1 then textFrame.text:SetText(L.RANGECHECK_RHEADERT:format(activeRange, warnThreshold)) else textFrame.text:SetText(L.RANGECHECK_RHEADER:format(activeRange)) end else if warnThreshold > 1 then textFrame.text:SetText(L.RANGECHECK_HEADERT:format(activeRange, warnThreshold)) else textFrame.text:SetText(L.RANGECHECK_HEADER:format(activeRange)) end end end if rEnabled and (prevRange ~= activeRange or prevThreshold ~= mainFrame.redCircleNumPlayers) then prevRange = activeRange pixelsperyard = min(radarFrame:GetWidth(), radarFrame:GetHeight()) / (activeRange * 3) radarFrame.circle:SetSize(activeRange * pixelsperyard * 2, activeRange * pixelsperyard * 2) if reverse then radarFrame.text:SetText(L.RANGERADAR_RHEADER:format(activeRange, mainFrame.redCircleNumPlayers)) else radarFrame.text:SetText(L.RANGERADAR_HEADER:format(activeRange, mainFrame.redCircleNumPlayers)) end end local playerMapId = GetBestMapForUnit("player") or 0 if not restricted then rotation = pi2 - (GetPlayerFacing() or 0) end local sinTheta = sin(rotation) local cosTheta = cos(rotation) local closePlayer = 0 local closestRange local closetName local filter = mainFrame.filter local type = reverse and 2 or filter and 1 or 0 local onlySummary = mainFrame.onlySummary for i = 1, GetNumGroupMembers() do local uId = unitList[i] local dot = radarFrame.dots[i] local mapId = GetBestMapForUnit(uId) or 0 if UnitExists(uId) and playerMapId == mapId and not UnitIsUnit(uId, "player") and not UnitIsDeadOrGhost(uId) and UnitIsConnected(uId) and UnitPhaseReasonHack(uId) and (not filter or filter(uId)) then local range = restricted and itsBCAgain(uId, activeRange) or UnitDistanceSquared(uId) ^ 0.5 local inRange = false if range < activeRange + 0.5 then closePlayer = closePlayer + 1 inRange = true if rEnabled then -- Only used by radar if not closestRange then closestRange = range elseif range < closestRange then closestRange = range end end if not closetName then closetName = DBM:GetUnitFullName(uId) closetName = DBM:GetShortServerName(closetName) end end if tEnabled and inRange and not onlySummary and closePlayer < 6 then -- Display up to 5 players in text range frame. local playerName = DBM:GetUnitFullName(uId) playerName = DBM:GetShortServerName(playerName) local color = RAID_CLASS_COLORS[dot.class] or NORMAL_FONT_COLOR textFrame.lines[closePlayer]:SetText(dot.icon and ("|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_%d:0|t %s"):format(dot.icon, playerName) or playerName) textFrame.lines[closePlayer]:SetTextColor(color.r, color.g, color.b) textFrame.lines[closePlayer]:Show() textFrame:SetHeight((closePlayer * 12) + 12) end if rEnabled then local playerX, playerY = UnitPosition("player") local x, y = UnitPosition(uId) if not x and not y then rangeCheck:Hide(true) return end dot.y = -(x - playerX) dot.x = -(y - playerY) dot.range = range setDot(i, sinTheta, cosTheta) end elseif rEnabled and dot:IsShown() then dot:Hide() end end if tEnabled then -- Green Text (Regular range frame and not near too many players, or reverse range frame and we ARE near enough) textFrame.inRangeText:SetText(L.RANGECHECK_IN_RANGE_TEXT:format(closePlayer, activeRange)) textFrame.inRangeText:Show() if (reverse and closePlayer >= warnThreshold) or (not reverse and closePlayer < warnThreshold) then textFrame.inRangeText:SetTextColor(0, 1, 0) -- Red Text (Regular range frame and we are near too many players, or reverse range frame and we aren't near enough) else updateSound(closePlayer) textFrame.inRangeText:SetTextColor(1, 0, 0) end textFrame:Show() end if rEnabled then if prevNumClosePlayer ~= closePlayer or prevclosestRange ~= closestRange or prevType ~= type then if closePlayer >= warnThreshold then -- Only show the text if the circle is red circleColor = reverse and 1 or 2 if closePlayer == 1 then radarFrame.inRangeText:SetText(L.RANGERADAR_IN_RANGE_TEXTONE:format(closetName, closestRange)) else radarFrame.inRangeText:SetText(L.RANGERADAR_IN_RANGE_TEXT:format(closePlayer, closestRange)) end radarFrame.inRangeText:Show() else circleColor = reverse and 2 or 1 radarFrame.inRangeText:Hide() end prevNumClosePlayer = closePlayer prevclosestRange = closestRange prevType = type end if UnitIsDeadOrGhost("player") then circleColor = 3 end if prevColor ~= circleColor then if circleColor == 1 then radarFrame.circle:SetVertexColor(0, 1, 0) elseif circleColor == 2 then radarFrame.circle:SetVertexColor(1, 0, 0) else radarFrame.circle:SetVertexColor(1, 1, 1) end prevColor = circleColor end if circleColor == 2 then -- Red updateSound(closePlayer) end end end end local updater = mainFrame:CreateAnimationGroup() updater:SetLooping("REPEAT") local anim = updater:CreateAnimation() anim:SetDuration(0.05) mainFrame:SetSize(0, 0) mainFrame:SetScript("OnEvent", function(self, event) if event == "GROUP_ROSTER_UPDATE" or event == "RAID_TARGET_UPDATE" then updateIcon() end end) --------------- -- Methods -- --------------- local restoreRange, restoreFilter, restoreThreshold, restoreReverse function rangeCheck:Show(range, filter, forceshow, redCircleNumPlayers, reverse, hideTime, onlySummary) if (DBM:GetNumRealGroupMembers() < 2 or DBM.Options.DontShowRangeFrame or DBM.Options.SpamSpecInformationalOnly) and not forceshow then return end local restrictionsActive = DBM:HasMapRestrictions() if not isWrath and restrictionsActive then--Don't popup on retail or classic era at all if in an instance (it now only works in wrath) return end if type(range) == "function" then -- The first argument is optional return self:Show(nil, range) end range = range or 10 redCircleNumPlayers = redCircleNumPlayers or 1 if not textFrame then createTextFrame() end if not radarFrame then createRadarFrame() end if restrictionsActive then range = setCompatibleRestrictedRange(range) end if (DBM.Options.RangeFrameFrames == "text" or DBM.Options.RangeFrameFrames == "both" or restrictionsActive) and not textFrame:IsShown() then textFrame:Show() end -- TODO, add check for restricted area here so we can prevent radar frame loading. if not restrictionsActive and (DBM.Options.RangeFrameFrames == "radar" or DBM.Options.RangeFrameFrames == "both") and not radarFrame:IsShown() then radarFrame:Show() end mainFrame.range = range mainFrame.filter = filter mainFrame.redCircleNumPlayers = redCircleNumPlayers mainFrame.reverse = reverse mainFrame.hideTime = hideTime and (GetTime() + hideTime) or 0 mainFrame.restrictions = restrictionsActive mainFrame.onlySummary = onlySummary if not mainFrame.eventRegistered then mainFrame.eventRegistered = true updateIcon() mainFrame:RegisterEvent("GROUP_ROSTER_UPDATE") mainFrame:RegisterEvent("RAID_TARGET_UPDATE") end updater:SetScript("OnLoop", updateRangeFrame) updater:Play() if forceshow and not DBM.Options.DontRestoreRange then -- Force means user activated range frame, store user value for restore function restoreRange, restoreFilter, restoreThreshold, restoreReverse = mainFrame.range, mainFrame.filter, mainFrame.redCircleNumPlayers, mainFrame.reverse end end function rangeCheck:Hide(force) if restoreRange and not force and (isWrath or not (mainFrame.restrictions or DBM:HasMapRestrictions())) then -- Restore range frame to way it was when boss mod is done with it rangeCheck:Show(restoreRange, restoreFilter, true, restoreThreshold, restoreReverse) else restoreRange, restoreFilter, restoreThreshold, restoreReverse = nil, nil, nil, nil updater:Stop() if mainFrame.eventRegistered then mainFrame.eventRegistered = nil mainFrame:UnregisterAllEvents() end if textFrame then textFrame:Hide() end if radarFrame then radarFrame:Hide() end end end function rangeCheck:IsShown() return textFrame and textFrame:IsShown() or radarFrame and radarFrame:IsShown() end function rangeCheck:IsRadarShown() return radarFrame and radarFrame:IsShown() end function rangeCheck:UpdateRestrictions(force) mainFrame.restrictions = force or DBM:HasMapRestrictions() if mainFrame.restrictions and not isWrath then rangeCheck:Hide(true) end end function rangeCheck:SetHideTime(hideTime) mainFrame.hideTime = hideTime and (GetTime() + hideTime) or 0 end function rangeCheck:GetDistance(...) return getDistanceBetween(...) end function rangeCheck:GetDistanceAll(checkrange) if DBM:HasMapRestrictions() then checkrange = setCompatibleRestrictedRange(checkrange) end return getDistanceBetweenAll(checkrange) end do local function UpdateLocalRangeFrame(r, reverse) if rangeCheck:IsShown() then rangeCheck:Hide(true) else if DBM:HasMapRestrictions() then DBM:AddMsg(L.TEXT_ONLY_RANGE) end rangeCheck:Show((r and r < 201) and r or 10 , nil, true, nil, reverse) end end SLASH_DBMRANGE1 = "/range" SLASH_DBMRANGE2 = "/distance" SLASH_DBMRRANGE1 = "/rrange" SLASH_DBMRRANGE2 = "/rdistance" SlashCmdList["DBMRANGE"] = function(msg) if not isWrath and DBM:HasMapRestrictions() then DBM:AddMsg(L.NO_RANGE) else UpdateLocalRangeFrame(tonumber(msg)) end end SlashCmdList["DBMRRANGE"] = function(msg) if not isWrath and DBM:HasMapRestrictions() then DBM:AddMsg(L.NO_RANGE) else UpdateLocalRangeFrame(tonumber(msg), true) end end end