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)