From d2cefe69cf91a6cfd60da826ab7c71cd11440aad Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Sat, 31 Oct 2026 17:06:59 -0500 Subject: [PATCH] Add a DBM-style range radar for party and raid members /radar toggles it and /radar sets the range; the right-click menu and the mouse wheel step through 5-40 yd. The map turns with GetPlayerFacing so ahead is up, and ClassicAPI.UnitPosition places class-colored dots (the mirrored name, since SuperWoW takes the global with x, y, z order). The range ring is two stacked TempPortraitAlphaMask discs: a ring-color disc under a slightly smaller background-color one. It turns red when a member is inside the range, and a list under the radar shows the five nearest with their raid markers inline. Raid markers come from the markN tokens, so marked mobs show too and a marked member shows the marker instead of a dot. Units behind terrain (UnitInLineOfSight, traced 4x a second) are dimmed. Units on another floor, over 5 yd up or down and steeper than 45 degrees so hills don't count, get an arrow above or below them. Hovering a blip shows its name, distance and both states; clicking a blip or a list row targets it. --- api/config.lua | 1 + init/modules.xml | 1 + modules/radar.lua | 373 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 375 insertions(+) create mode 100644 modules/radar.lua diff --git a/api/config.lua b/api/config.lua index 568a6ea8..5cf5f32d 100644 --- a/api/config.lua +++ b/api/config.lua @@ -984,6 +984,7 @@ function pfUI:LoadConfig() pfUI:UpdateConfig("equipmentmanager", nil, "unusedquality", "1") pfUI:UpdateConfig("equipmentmanager", nil, "unusedbank", "1") pfUI:UpdateConfig("character", "reputation", "repRequired", "1") + pfUI:UpdateConfig("radar", nil, "range", "10") pfUI:UpdateConfig("thirdparty", nil, "chatbg", "1") pfUI:UpdateConfig("thirdparty", nil, "showmeter", "0") pfUI:UpdateConfig("thirdparty", "dpsmate", "skin", "0") diff --git a/init/modules.xml b/init/modules.xml index 5ae46479..74968298 100644 --- a/init/modules.xml +++ b/init/modules.xml @@ -83,4 +83,5 @@ + \ No newline at end of file diff --git a/modules/radar.lua b/modules/radar.lua new file mode 100644 index 00000000..96103b84 --- /dev/null +++ b/modules/radar.lua @@ -0,0 +1,373 @@ +pfUI:RegisterModule("radar", function () + -- DBM-style range radar: the group around you, turned so that your facing + -- points up. ClassicAPI's UnitPosition reads every visible unit. + local SIZE = 150 + local RADIUS = SIZE / 2 + -- the range ring sits at two thirds of the radius, so the radar shows half + -- a range past it + local RING = RADIUS / 1.5 + local RANGES = { 5, 8, 10, 15, 20, 25, 30, 40 } + -- about one storey; see Locate for what counts as another floor + local FLOOR = 5 + -- alpha of units out of sight + local DIM = .3 + -- the nearest members inside the range, listed under the radar + local LINES = 5 + local LINE_HEIGHT = C.global.font_size + 4 + + local _, border = GetBorderSize() + local abs, sqrt = math.abs, math.sqrt + local UnitPosition = ClassicAPI.UnitPosition + + local radar = CreateFrame("Frame", "pfRadar", UIParent) + radar:SetSize(SIZE, SIZE) + radar:SetPoint("CENTER", 350, 100) + radar:SetFrameStrata("MEDIUM") + radar:EnableMouse(true) + radar:EnableMouseWheel(true) + radar:Hide() + CreateBackdrop(radar) + CreateBackdropShadow(radar) + UpdateMovable(radar) + + -- the range ring: a disc in the ring color under a 2px smaller disc in the + -- background color, which lets a little of the tint through + local DISC = "Interface\\CharacterFrame\\TempPortraitAlphaMask" + radar.ring = radar:CreateTexture(nil, "BACKGROUND") + radar.ring:SetTexture(DISC) + radar.ring:SetSize(RING * 2 + 2, RING * 2 + 2) + radar.ring:SetPoint("CENTER", 0, 0) + + radar.zone = radar:CreateTexture(nil, "BORDER") + radar.zone:SetTexture(DISC) + radar.zone:SetSize(RING * 2 - 2, RING * 2 - 2) + radar.zone:SetPoint("CENTER", 0, 0) + local r, g, b = GetStringColor(C.appearance.border.background) + radar.zone:SetVertexColor(r, g, b, .85) + + radar.player = radar:CreateTexture(nil, "OVERLAY") + radar.player:SetAtlas("MinimapArrow") + radar.player:SetSize(24, 24) + radar.player:SetPoint("CENTER", 0, 0) + + radar.title = radar:CreateFontString(nil, "OVERLAY") + radar.title:SetFont(pfUI.font_default, C.global.font_size, "OUTLINE") + radar.title:SetPoint("TOPLEFT", 4, -4) + + radar.count = radar:CreateFontString(nil, "OVERLAY") + radar.count:SetFont(pfUI.font_default, C.global.font_size, "OUTLINE") + radar.count:SetPoint("TOPRIGHT", -4, -4) + radar.count:SetTextColor(1, .2, .2) + + radar.names = CreateFrame("Frame", nil, radar) + radar.names:SetPoint("TOPLEFT", radar, "BOTTOMLEFT", 0, -border * 3) + radar.names:SetPoint("TOPRIGHT", radar, "BOTTOMRIGHT", 0, -border * 3) + radar.names:Hide() + CreateBackdrop(radar.names) + CreateBackdropShadow(radar.names) + + local range, scale + + local function SetRange(yards) + range = yards + scale = RING / yards + C.radar.range = tostring(yards) + radar.title:SetText(T["Radar"] .. " (" .. yards .. " yd)") + end + SetRange(tonumber(C.radar.range)) + + local function IsRange(yards) + return yards == range + end + + local function GenerateMenu(owner, rootDescription) + rootDescription:CreateTitle(T["Radar"]) + for _, yards in ipairs(RANGES) do + rootDescription:CreateRadio(yards .. " yd", IsRange, SetRange, yards) + end + rootDescription:CreateDivider() + rootDescription:CreateButton(HIDE, function() radar:Hide() end) + end + + radar.menuMixin = pfUIMenuStyleMixin + local function OpenMenu() + MenuUtil.CreateContextMenu(radar, GenerateMenu) + end + + -- blips and list rows: left click targets, right click opens the menu + local function TargetOrMenu() + if arg1 == "RightButton" then + OpenMenu() + else + TargetUnit(this.unit) + end + end + + -- unit -> true while terrain blocks the line of sight + local blocked = {} + local hovered + + local function ShowTooltip(blip) + local unit = blip.unit + GameTooltip:SetOwner(blip, "ANCHOR_RIGHT") + if UnitIsPlayer(unit) then + GameTooltip:SetText(UnitName(unit), PFUI_CLASS_COLORS[UnitClassBase(unit)]:GetRGB()) + else + GameTooltip:SetText(UnitName(unit), UnitSelectionColor(unit)) + end + GameTooltip:AddLine(format("%.1f yd", sqrt(blip.distSq)), 1, 1, 1) + if blocked[unit] then + GameTooltip:AddLine(T["Out of line of sight"], 1, .3, .3) + end + if blip.level then + GameTooltip:AddLine(blip.level > 0 and T["Above you"] or T["Below you"], .6, .6, .6) + end + GameTooltip:Show() + end + + -- a unit on the radar: hover for its tooltip, click to target it + local function CreateBlip(size, layer) + local blip = CreateFrame("Button", nil, radar) + blip:SetSize(size, size) + blip:SetFrameLevel(radar:GetFrameLevel() + layer) + blip:SetHitRectInsets(-2, -2, -2, -2) + blip:RegisterForClicks("LeftButtonUp", "RightButtonUp") + blip:Hide() + + blip.icon = blip:CreateTexture(nil, "ARTWORK") + blip.icon:SetAllPoints(blip) + + -- sits above or below a unit on another floor; the triangle fills the + -- middle half of img\up and img\down + blip.arrow = blip:CreateTexture(nil, "OVERLAY") + blip.arrow:SetSize(10, 5) + + blip:SetScript("OnClick", TargetOrMenu) + blip:SetScript("OnEnter", function() + hovered = this + ShowTooltip(this) + end) + blip:SetScript("OnLeave", function() + hovered = nil + GameTooltip:Hide() + end) + return blip + end + + local dots = {} + local function GetDot(i) + if not dots[i] then + dots[i] = CreateBlip(8, 1) + dots[i].icon:SetTexture(pfUI.media["img:circleraid"]) + end + return dots[i] + end + + -- above the dots + local markers = {} + for i = 1, 8 do + markers[i] = CreateBlip(14, 2) + markers[i].icon:SetAtlas("UI-RaidTargetingIcon_" .. i) + end + + local rows = {} + for i = 1, LINES do + local row = CreateFrame("Button", nil, radar.names) + row:SetPoint("TOPLEFT", 4, -2 - (i - 1) * LINE_HEIGHT) + row:SetPoint("TOPRIGHT", -4, -2 - (i - 1) * LINE_HEIGHT) + row:SetHeight(LINE_HEIGHT) + row:RegisterForClicks("LeftButtonUp", "RightButtonUp") + row:SetScript("OnClick", TargetOrMenu) + + row.name = row:CreateFontString(nil, "OVERLAY") + row.name:SetFont(pfUI.font_default, C.global.font_size, "OUTLINE") + row.name:SetPoint("LEFT", 0, 0) + row.dist = row:CreateFontString(nil, "OVERLAY") + row.dist:SetFont(pfUI.font_default, C.global.font_size, "OUTLINE") + row.dist:SetPoint("RIGHT", 0, 0) + rows[i] = row + end + + -- the LINES nearest members inside the range, nearest first + local nearUnit, nearDistSq = {}, {} + local function AddNear(listed, unit, distSq) + if listed == LINES and distSq >= nearDistSq[LINES] then return listed end + local i = listed < LINES and listed + 1 or LINES + while i > 1 and nearDistSq[i - 1] > distSq do + nearUnit[i], nearDistSq[i] = nearUnit[i - 1], nearDistSq[i - 1] + i = i - 1 + end + nearUnit[i], nearDistSq[i] = unit, distSq + return listed < LINES and listed + 1 or LINES + end + + -- the player's position and facing, read once per refresh + local py, px, pz, facing + + -- where a unit shows on the radar in pixels, its squared distance, and 1 + -- or -1 on a floor above or below you. nil past the edge. The world turns + -- clockwise by the facing, so ahead is up. + local function Locate(unit) + if not py or not facing then return end + if UnitIsUnit(unit, "player") or UnitIsDeadOrGhost(unit) then return end + + local uy, ux, uz = UnitPosition(unit) + if not uy then return end + + local west, north, up = uy - py, ux - px, uz - pz + local x, y = Vector2D_RotateDirection(-facing, -west, north) + x, y = x * scale, y * scale + if abs(x) >= RADIUS or abs(y) >= RADIUS then return end + + -- another floor is a storey up or down and steeper than 45 degrees, which + -- no walkable slope is, so units up a hill don't count + local level + if abs(up) > FLOOR and up * up > west * west + north * north then + level = up > 0 and 1 or -1 + end + return x, y, Vector3D_GetLengthSquared(west, north, up), level + end + + local function Place(blip, unit, x, y, distSq, level, checkLoS) + if checkLoS then blocked[unit] = UnitInLineOfSight(unit) == false end + blip.unit, blip.distSq, blip.level = unit, distSq, level + blip:SetPoint("CENTER", x, y) + blip:SetAlpha(blocked[unit] and DIM or 1) + if level then + blip.arrow:SetTexture(pfUI.media[level > 0 and "img:up" or "img:down"]) + blip.arrow:SetTexCoord(0, 1, .25, .75) + blip.arrow:ClearAllPoints() + if level > 0 then + blip.arrow:SetPoint("BOTTOM", blip, "TOP", 0, 1) + else + blip.arrow:SetPoint("TOP", blip, "BOTTOM", 0, -1) + end + end + blip.arrow:SetShown(level ~= nil) + blip:Show() + end + + function radar:Refresh(checkLoS) + py, px, pz = UnitPosition("player") + facing = GetPlayerFacing() + local shown, inRange, listed = 0, 0, 0 + + local raid = IsInRaid() + local prefix = raid and "raid" or "party" + for i = 1, raid and GetNumRaidMembers() or GetNumPartyMembers() do + local unit = prefix .. i + local x, y, distSq, level = Locate(unit) + if x then + -- inside the range always means inside the radar + if distSq <= range * range then + inRange = inRange + 1 + listed = AddNear(listed, unit, distSq) + end + + -- a marked member shows the marker instead of a dot + if not GetRaidTargetIndex(unit) then + shown = shown + 1 + local dot = GetDot(shown) + dot.icon:SetVertexColor(PFUI_CLASS_COLORS[UnitClassBase(unit)]:GetRGB()) + Place(dot, unit, x, y, distSq, level, checkLoS) + end + end + end + + for i = shown + 1, #dots do dots[i]:Hide() end + + -- markN resolves to whatever unit wears marker N, mobs included + for i = 1, 8 do + local unit = "mark" .. i + local x, y, distSq, level = Locate(unit) + if x then + Place(markers[i], unit, x, y, distSq, level, checkLoS) + else + markers[i]:Hide() + end + end + + for i = 1, LINES do + local row = rows[i] + if i <= listed then + local unit = nearUnit[i] + local marker = GetRaidTargetIndex(unit) + local name = UnitName(unit) + if marker then + -- the markup's own tint is white, so the text color leaves it alone + name = name .. " " .. CreateAtlasMarkup("UI-RaidTargetingIcon_" .. marker, 14, 14) + end + row.name:SetText(name) + row.name:SetTextColor(PFUI_CLASS_COLORS[UnitClassBase(unit)]:GetRGB()) + row.dist:SetFormattedText("%.1f", sqrt(nearDistSq[i])) + row.unit = unit + end + row:SetShown(i <= listed) + end + self.names:SetHeight(listed * LINE_HEIGHT + 4) + self.names:SetShown(listed > 0) + + if inRange > 0 then + self.ring:SetVertexColor(1, .2, .2) + self.count:SetText(inRange) + else + self.ring:SetVertexColor(.2, 1, .2) + self.count:SetText("") + end + + -- keep the hovered tooltip's distance current + if hovered then + if hovered:IsShown() then + ShowTooltip(hovered) + else + hovered = nil + GameTooltip:Hide() + end + end + end + + -- line of sight traces run at a slower rate than the positions + local throttle, losThrottle = 0, 1 + radar:SetScript("OnUpdate", function() + throttle = throttle + arg1 + losThrottle = losThrottle + arg1 + if throttle < .05 then return end + throttle = 0 + + local checkLoS = losThrottle >= .25 + if checkLoS then losThrottle = 0 end + this:Refresh(checkLoS) + end) + + radar:SetScript("OnMouseUp", function() + if arg1 == "RightButton" then OpenMenu() end + end) + + -- the wheel steps through the ranges, up zooms in + radar:SetScript("OnMouseWheel", function() + local yards = range + if arg1 > 0 then + for i = #RANGES, 1, -1 do + if RANGES[i] < range then yards = RANGES[i] break end + end + else + for i = 1, #RANGES do + if RANGES[i] > range then yards = RANGES[i] break end + end + end + SetRange(yards) + end) + + -- /radar toggles, /radar sets the range and shows + pfUI.api.RegisterSlashCommand("PFRADAR", { "/radar", "/pfradar" }, function(msg) + local yards = tonumber(msg) + if yards and yards > 0 then + SetRange(yards) + radar:Show() + else + radar:SetShown(not radar:IsShown()) + end + end) + + pfUI.radar = radar +end)