Files
RelationshipsPVP/RelationshipsPVP.lua
T
2026-07-13 19:43:19 +01:00

1109 lines
35 KiB
Lua

-- Relationships PVP
RelationshipsPVPCharDB = RelationshipsPVPCharDB or {}
local BG_KEYS = { "AV", "WSG", "AB" }
local ARENA_KEYS = { "A2", "A3", "A5", "SK" }
local ALL_KEYS = { "AV", "WSG", "AB", "A2", "A3", "A5", "SK" }
local BG_NAMES = {
AV = "Alterac Valley",
WSG = "Warsong Gulch",
AB = "Arathi Basin",
A2 = "2v2 Rated",
A3 = "3v3 Rated",
A5 = "5v5 Rated",
SK = "Skirmish",
}
local BG_COLORS = {
AV = { r = 0.6, g = 0.85, b = 1.0 },
WSG = { r = 0.6, g = 1.0, b = 0.6 },
AB = { r = 1.0, g = 0.85, b = 0.4 },
A2 = { r = 1.0, g = 0.6, b = 0.9 },
A3 = { r = 1.0, g = 0.5, b = 0.6 },
A5 = { r = 0.9, g = 0.5, b = 1.0 },
SK = { r = 0.8, g = 0.8, b = 0.8 },
}
-- Arena bracket meta: teamSize + isRated (registeredMatch)
local ARENA_META = {
A2 = { size = 2, rated = 1 },
A3 = { size = 3, rated = 1 },
A5 = { size = 5, rated = 1 },
SK = { size = 2, rated = 0 }, -- default skirmish to 2v2 unrated
}
-- TurtleWoW / OctoWoW arena finder ids for JoinArenaQueue(id).
-- Skirmish must be 0; passing the team size opens the Blood Ring panel
-- without completing the queue.
local ARENA_IDS = {
SK = 0,
A2 = 1,
A3 = 2,
A5 = 3,
}
local MARKS_WIN = 3
local MARKS_LOSS = 1
local currentBG = nil
local endReported = {}
-- Track the last arena bracket the user queued for, so we can classify
-- the resulting match even on clients that don't report teamSize.
local lastQueuedArena = nil
-- Runtime UI (row FontStrings created dynamically)
local rowStrings = {}
local totalRow = nil
---------------------------------------------------------------
-- Utility
---------------------------------------------------------------
local function NewStatBlock()
return {
wins = 0, draws = 0, losses = 0,
kills = 0, deaths = 0, hk = 0,
marks = 0, honor = 0,
bestKB = 0, streak = 0, bestStreak = 0,
}
end
local function EnsureDB()
local db = RelationshipsPVPCharDB
db.stats = db.stats or {}
db.icon = db.icon or { point = "CENTER", x = 60, y = -60 }
if db.showIcon == nil then db.showIcon = true end
for i = 1, table.getn(ALL_KEYS) do
local k = ALL_KEYS[i]
local s = db.stats[k] or NewStatBlock()
if s.honor == nil then s.honor = 0 end
if s.bestKB == nil then s.bestKB = 0 end
if s.streak == nil then s.streak = 0 end
if s.bestStreak == nil then s.bestStreak = 0 end
if s.hk == nil then s.hk = 0 end
if s.marks == nil then s.marks = 0 end
if s.draws == nil then s.draws = 0 end
db.stats[k] = s
end
return db
end
-- Clamp a UIParent-relative CENTER offset so the button stays visible on screen.
function RelationshipsPVP_ClampScreenOffset(x, y)
x = x or 0
y = y or 0
if not UIParent or not UIParent.GetWidth then return x, y end
local w = UIParent:GetWidth() or 1024
local h = UIParent:GetHeight() or 768
local pad = -60
local maxX = (w / 2) - pad
local maxY = (h / 2) - pad
if x > maxX then x = maxX end
if x < -maxX then x = -maxX end
if y > maxY then y = maxY end
if y < -maxY then y = -maxY end
return x, y
end
-- Position the icon anywhere on screen (parented to UIParent for free drag).
function RelationshipsPVP_SetIconPosition(x, y, save)
local btn = RelationshipsPVPMinimapButton
if not btn or not UIParent then return end
x, y = RelationshipsPVP_ClampScreenOffset(x, y)
btn:ClearAllPoints()
btn:SetParent(UIParent)
btn:SetPoint("CENTER", UIParent, "CENTER", x, y)
if btn.SetFrameStrata then btn:SetFrameStrata("MEDIUM") end
if btn.SetFrameLevel then btn:SetFrameLevel(10) end
if save then
local db = EnsureDB()
db.icon.point = "CENTER"
db.icon.relPoint = "CENTER"
db.icon.parent = "UIParent"
db.icon.x = x
db.icon.y = y
end
end
-- Read the button's current center in UIParent coordinates and persist it.
function RelationshipsPVP_SaveIconPos()
local db = EnsureDB()
local btn = RelationshipsPVPMinimapButton
if not btn or not UIParent then return end
local x = db.icon.x or 0
local y = db.icon.y or 0
if btn.GetCenter and UIParent.GetCenter then
local bx, by = btn:GetCenter()
local ux, uy = UIParent:GetCenter()
if bx and by and ux and uy then
x = bx - ux
y = by - uy
end
end
x, y = RelationshipsPVP_ClampScreenOffset(x, y)
db.icon.point = "CENTER"
db.icon.relPoint = "CENTER"
db.icon.parent = "UIParent"
db.icon.x = x
db.icon.y = y
end
-- Free drag: reparent to UIParent and let the client move the frame.
function RelationshipsPVP_StartIconDrag()
local btn = RelationshipsPVPMinimapButton
if not btn then return end
btn:SetParent(UIParent)
if btn.SetFrameStrata then btn:SetFrameStrata("MEDIUM") end
if btn.SetFrameLevel then btn:SetFrameLevel(10) end
if btn.StartMoving then btn:StartMoving() end
end
function RelationshipsPVP_StopIconDrag()
local btn = RelationshipsPVPMinimapButton
if not btn then return end
if btn.StopMovingOrSizing then btn:StopMovingOrSizing() end
RelationshipsPVP_SaveIconPos()
local db = EnsureDB()
-- Re-anchor cleanly to a single CENTER/CENTER offset so the saved value is
-- exactly what we restore on next login.
RelationshipsPVP_SetIconPosition(db.icon.x, db.icon.y, 1)
end
local function ApplyIconVisibility()
local db = EnsureDB()
local btn = RelationshipsPVPMinimapButton
if not btn then return end
if db.showIcon then btn:Show() else btn:Hide() end
end
-- Loading screens / entering BGs can briefly hide parent frames; re-assert
-- our intended visibility so the icon never "randomly disappears".
local iconWatcher
local function InstallIconWatcher()
if iconWatcher then return end
iconWatcher = CreateFrame("Frame")
iconWatcher.elapsed = 0
iconWatcher:SetScript("OnUpdate", function()
iconWatcher.elapsed = (iconWatcher.elapsed or 0) + arg1
if iconWatcher.elapsed < 1.0 then return end
iconWatcher.elapsed = 0
local db = RelationshipsPVPCharDB
if not db then return end
local btn = RelationshipsPVPMinimapButton
if not btn then return end
if db.showIcon then
if not btn:IsShown() then btn:Show() end
else
if btn:IsShown() then btn:Hide() end
end
end)
end
local function RestoreIconPos()
local db = EnsureDB()
local btn = RelationshipsPVPMinimapButton
if not btn then return end
-- Migrate any prior Minimap-relative save into a UIParent-relative offset
-- so free-drag mode has a valid starting position.
if db.icon.parent == "Minimap" and Minimap and Minimap.GetCenter and UIParent and UIParent.GetCenter then
local mx, my = Minimap:GetCenter()
local ux, uy = UIParent:GetCenter()
if mx and my and ux and uy then
db.icon.x = (mx - ux) + (db.icon.x or 0)
db.icon.y = (my - uy) + (db.icon.y or 0)
end
db.icon.parent = "UIParent"
db.icon.point = "CENTER"
db.icon.relPoint = "CENTER"
end
RelationshipsPVP_SetIconPosition(db.icon.x or 0, db.icon.y or 0, 1)
ApplyIconVisibility()
InstallIconWatcher()
end
local function SavePanelPos()
local db = EnsureDB()
db.panel = db.panel or {}
if not RelationshipsPVPFrame then return end
local point, _, _, x, y = RelationshipsPVPFrame:GetPoint()
db.panel.point = point or "CENTER"
db.panel.x = x or 0
db.panel.y = y or 0
end
local function RestorePanelPos()
local db = EnsureDB()
if not RelationshipsPVPFrame or not db.panel or not db.panel.point then return end
RelationshipsPVPFrame:ClearAllPoints()
RelationshipsPVPFrame:SetPoint(db.panel.point, UIParent, db.panel.point, db.panel.x, db.panel.y)
end
local function Print(msg)
if DEFAULT_CHAT_FRAME then
DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffRelationships PVP:|r " .. msg)
end
end
local function MapNameToKey(name)
if not name then return nil end
if name == BG_NAMES.AV then return "AV" end
if name == BG_NAMES.WSG then return "WSG" end
if name == BG_NAMES.AB then return "AB" end
return nil
end
local function FormatNumber(n)
n = n or 0
if n >= 1000000 then
return string.format("%.1fm", n / 1000000)
elseif n >= 10000 then
return string.format("%.1fk", n / 1000)
end
return tostring(n)
end
---------------------------------------------------------------
-- BG / Arena detection
---------------------------------------------------------------
local function ClassifyBattlefield(i)
local status, mapName, instanceID, minLevel, maxLevel, teamSize, registered = GetBattlefieldStatus(i)
if status ~= "active" then return nil end
local bgKey = MapNameToKey(mapName)
if bgKey then return bgKey end
if teamSize and teamSize > 0 then
if registered == 1 or registered == true then
if teamSize == 2 then return "A2" end
if teamSize == 3 then return "A3" end
if teamSize == 5 then return "A5" end
else
return "SK"
end
end
if lastQueuedArena then return lastQueuedArena end
return nil
end
local function DetectCurrentBG()
for i = 1, 3 do
local status = GetBattlefieldStatus(i)
if status == "active" then
local key = ClassifyBattlefield(i)
if key then return key, i end
end
end
return nil, nil
end
local function RecordEnd(key, index)
if not key or endReported[index] then return end
endReported[index] = true
local db = EnsureDB()
local s = db.stats[key]
if not s then return end
local playerWon = false
local isDraw = false
local winner = GetBattlefieldWinner()
if winner then
local myFaction = UnitFactionGroup("player")
-- Some server builds (incl. Turtle/Octo) report a draw as a winner
-- value that is neither 0 (Horde) nor 1 (Alliance). Treat anything
-- else as a draw so it isn't miscounted as a loss.
isDraw = (winner ~= 0 and winner ~= 1)
if isDraw then
s.draws = s.draws + 1
s.streak = 0
else
local wonAsAlliance = (winner == 1 and myFaction == "Alliance")
local wonAsHorde = (winner == 0 and myFaction == "Horde")
playerWon = wonAsAlliance or wonAsHorde
if playerWon then
s.wins = s.wins + 1
s.streak = s.streak + 1
if s.streak > s.bestStreak then s.bestStreak = s.streak end
else
s.losses = s.losses + 1
s.streak = 0
end
end
local isBG = (key == "AV" or key == "WSG" or key == "AB")
if isBG then
-- Draws award the loss-tier mark count.
s.marks = s.marks + (playerWon and MARKS_WIN or MARKS_LOSS)
end
end
RequestBattlefieldScoreData()
local me = UnitName("player")
local rows = GetNumBattlefieldScores() or 0
for r = 1, rows do
local name, killingBlows, honorableKills, deaths, honorGained = GetBattlefieldScore(r)
if name == me then
local kb = killingBlows or 0
local hk = honorableKills or 0
local dth = deaths or 0
local hon = honorGained or 0
s.kills = s.kills + kb
s.hk = s.hk + hk
s.deaths = s.deaths + dth
s.honor = s.honor + hon
if kb > s.bestKB then s.bestKB = kb end
break
end
end
local marksMsg = ""
if key == "AV" or key == "WSG" or key == "AB" then
marksMsg = " +" .. (playerWon and MARKS_WIN or MARKS_LOSS) .. " marks"
end
local outcomeStr
if isDraw then
outcomeStr = "|cffffff66draw|r"
elseif playerWon then
outcomeStr = "|cff66ff66win|r"
else
outcomeStr = "|cffff6666loss|r"
end
Print(BG_NAMES[key] .. " recorded (" .. outcomeStr .. marksMsg .. ").")
end
---------------------------------------------------------------
-- Queue actions
---------------------------------------------------------------
-- Auto-close the Blizzard PvP panels if they pop up as a side effect
-- of the arena / BG queueing calls.
-- NOTE: We deliberately do NOT call HideUIPanel() here. HideUIPanel on a
-- frame that Blizzard's UI panel system has slotted into a left/center
-- slot triggers a slot reflow, which is what caused Character / Friends /
-- Spellbook panels to jump to the middle of the screen after the PvP
-- panel was opened. Directly calling :Hide() bypasses the slot manager
-- entirely; the slot suppression below (SuppressUIPanelWindows) ensures
-- Blizzard never registered a slot for these frames to begin with.
local function HidePvPPanels()
if BattlefieldFrame and BattlefieldFrame:IsVisible() then BattlefieldFrame:Hide() end
if PVPBattlegroundFrame and PVPBattlegroundFrame:IsVisible() then PVPBattlegroundFrame:Hide() end
if PVPFrame and PVPFrame:IsVisible() then PVPFrame:Hide() end
if HonorFrame and HonorFrame:IsVisible() then HonorFrame:Hide() end
end
-- Suppress the PvP panel entirely while we are auto-queueing. Reference-counted
-- so nested queues (Queue All BGs) do not release too early.
--
-- IMPORTANT: We previously hijacked BattlefieldFrame.Show / PVPBattlegroundFrame.Show
-- to no-op during suppression. That was the source of the "Character / Friends
-- panels jump to the middle of the screen" bug: Blizzard's ShowUIPanel() first
-- allocates a UI panel slot (left / center / doublewide) via UIPanelWindows,
-- THEN calls frame:Show(). Silently dropping :Show() left the slot reserved by
-- an invisible frame, so the next panel the user opened (Character, Friends,
-- Spellbook, ...) got shoved into a different slot -> centered.
--
-- The correct approach on 1.12 is to temporarily remove these frames from
-- UIPanelWindows while suppressing, so ShowUIPanel treats them as plain
-- frames and never allocates a slot. When suppression ends, the original
-- UIPanelWindows entries are restored so normal Blizzard UI behaviour is
-- preserved for the rest of the session.
local suppressPanels = 0
local savedUIPanelEntries = nil
local SUPPRESSED_PANEL_NAMES = {
"BattlefieldFrame",
"PVPBattlegroundFrame",
"PVPFrame",
"HonorFrame",
}
local function SuppressUIPanelWindows()
if savedUIPanelEntries then return end
savedUIPanelEntries = {}
if type(UIPanelWindows) ~= "table" then return end
for i = 1, table.getn(SUPPRESSED_PANEL_NAMES) do
local name = SUPPRESSED_PANEL_NAMES[i]
savedUIPanelEntries[name] = UIPanelWindows[name]
UIPanelWindows[name] = nil
end
end
local function RestoreUIPanelWindows()
if not savedUIPanelEntries then return end
if type(UIPanelWindows) == "table" then
for i = 1, table.getn(SUPPRESSED_PANEL_NAMES) do
local name = SUPPRESSED_PANEL_NAMES[i]
UIPanelWindows[name] = savedUIPanelEntries[name]
end
end
savedUIPanelEntries = nil
end
local function BeginSuppress()
suppressPanels = suppressPanels + 1
SuppressUIPanelWindows()
HidePvPPanels()
end
local function EndSuppress()
suppressPanels = suppressPanels - 1
if suppressPanels < 0 then suppressPanels = 0 end
HidePvPPanels()
if suppressPanels == 0 then
RestoreUIPanelWindows()
end
end
-- Central pending-queue state. `pendingQueue` is a FIFO of items:
-- { kind = "bg"|"arena", key = <BG_KEY>, asGroup = 0/1 }
-- The finder frame processes them one at a time on BATTLEFIELDS_SHOW.
local pendingQueue = {}
local finderFrame
-- Kick the client into opening the finder for the requested queue type.
-- The BATTLEFIELDS_SHOW handler below immediately completes the queue and
-- hides the Blizzard/Blood Ring panel.
local function KickFinderFor(job)
if job.kind == "arena" then
if type(JoinArenaQueue) == "function" then
local id = ARENA_IDS[job.key]
if id == nil then id = 0 end
JoinArenaQueue(id)
return true
end
if type(JoinBattlegroundQueue) == "function" then
JoinBattlegroundQueue(BG_NAMES[job.key])
return true
end
return false
end
if type(JoinBattlegroundQueue) == "function" then
JoinBattlegroundQueue(BG_NAMES[job.key])
return true
end
return false
end
local function GetFinderFrame()
if finderFrame then return finderFrame end
finderFrame = CreateFrame("Frame")
finderFrame:SetScript("OnEvent", function()
if event ~= "BATTLEFIELDS_SHOW" then return end
local job = pendingQueue[1]
if not job then
HidePvPPanels()
EndSuppress()
return
end
table.remove(pendingQueue, 1)
if SetSelectedBattlefield then SetSelectedBattlefield(0) end
if job.kind == "arena" then
local meta = ARENA_META[job.key] or ARENA_META.SK
lastQueuedArena = job.key
-- Prefer explicit arena team-size setters where available.
if type(SetArenaTeamSize) == "function" then SetArenaTeamSize(meta.size) end
if JoinBattlefield then
-- Turtle/Octo: JoinArenaQueue(id) opens Blood Ring, then
-- JoinBattlefield(0[, asGroup]) actually joins First Available.
if job.asGroup == 1 then JoinBattlefield(0, 1) else JoinBattlefield(0) end
end
else
if JoinBattlefield then
if job.asGroup == 1 then JoinBattlefield(0, 1) else JoinBattlefield(0) end
end
end
HidePvPPanels()
EndSuppress()
-- Chain: if more queued, kick the next one after a very short delay.
if pendingQueue[1] then
local nxt = pendingQueue[1]
BeginSuppress()
finderFrame:RegisterEvent("BATTLEFIELDS_SHOW")
if not KickFinderFor(nxt) then
table.remove(pendingQueue, 1)
finderFrame:UnregisterEvent("BATTLEFIELDS_SHOW")
EndSuppress()
end
else
finderFrame:UnregisterEvent("BATTLEFIELDS_SHOW")
end
end)
return finderFrame
end
local function IsInGroup()
if RelationshipsPVPGroupCheck and RelationshipsPVPGroupCheck:GetChecked() then return 1 end
return 0
end
local function EnqueueAndKick(job)
local f = GetFinderFrame()
-- If another job is already in flight, just append; the handler will chain.
if pendingQueue[1] then
table.insert(pendingQueue, job)
return true
end
table.insert(pendingQueue, job)
BeginSuppress()
f:UnregisterAllEvents()
f:RegisterEvent("BATTLEFIELDS_SHOW")
if not KickFinderFor(job) then
pendingQueue = {}
f:UnregisterEvent("BATTLEFIELDS_SHOW")
EndSuppress()
return false
end
return true
end
function RelationshipsPVP_Queue(key)
if not BG_NAMES[key] then return end
local asGroup = IsInGroup()
if EnqueueAndKick({ kind = "bg", key = key, asGroup = asGroup }) then
Print("Queued: |cffffd200" .. BG_NAMES[key] .. "|r"
.. (asGroup == 1 and " |cff66ff66(group)|r" or ""))
return
end
-- Fallback: legacy vanilla flow (visit a Battlemaster first).
if not JoinBattlefield then
Print("|cffff4444Battleground Finder not available on this client.|r")
return
end
local idx
if key == "AV" then idx = 1
elseif key == "WSG" then idx = 2
elseif key == "AB" then idx = 3 end
JoinBattlefield(idx, asGroup)
Print("Requested queue: |cffffd200" .. BG_NAMES[key] .. "|r")
end
---------------------------------------------------------------
-- Arena queue: fully automated, no manual panel confirmation
---------------------------------------------------------------
-- Turtle/Octo Blood Ring panel: JoinArenaQueue(id) opens a custom
-- frame (typically BloodRingFrame). It does NOT fire BATTLEFIELDS_SHOW
-- and JoinBattlefield does not apply, so we simulate a click on the
-- panel's own Join button.
-- Text labels the Blood Ring / arena finder Join button may carry.
local ARENA_JOIN_TEXTS = {
["join battle"] = 1, ["join queue"] = 1, ["join"] = 1,
["enter queue"] = 1, ["enter battle"] = 1, ["queue"] = 1,
["enter"] = 1, ["join arena"] = 1, ["queue up"] = 1,
}
local function _btnText(btn)
if not btn then return nil end
if btn.GetText then
local t = btn:GetText()
if t and t ~= "" then return t end
end
local name = btn.GetName and btn:GetName()
if name then
local fs = getglobal(name.."Text")
if fs and fs.GetText then return fs:GetText() end
end
return nil
end
local function _looksLikeJoin(btn)
if not btn or type(btn.GetObjectType) ~= "function" then return false end
if btn:GetObjectType() ~= "Button" then return false end
if btn.IsVisible and not btn:IsVisible() then return false end
if btn.IsEnabled and btn:IsEnabled() == 0 then return false end
local t = _btnText(btn)
if not t then return false end
return ARENA_JOIN_TEXTS[string.lower(t)] == 1
end
-- Locate the Blood Ring frame by likely names or a visible frame carrying
-- a Join-style child button.
local function FindArenaFinderFrame()
local names = {
"BloodRingFrame", "BloodringFrame", "BloodRingArenaFrame",
"ArenaFinderFrame", "ArenaQueueFrame", "SkirmishFrame",
"CustomArenaFrame", "PVPArenaFrame",
}
for i = 1, table.getn(names) do
local f = getglobal(names[i])
if f and f.IsVisible and f:IsVisible() then return f end
end
return nil
end
local function FindJoinButtonIn(frame)
if not frame then return nil end
-- Try predictable child names first.
local name = frame.GetName and frame:GetName()
if name then
local suffixes = {
"JoinButton", "JoinBattleButton", "QueueButton", "EnterButton",
"Button1", "OKButton", "AcceptButton",
}
for i = 1, table.getn(suffixes) do
local b = getglobal(name .. suffixes[i])
if _looksLikeJoin(b) then return b end
end
end
-- Walk direct children.
if frame.GetChildren then
local kids = { frame:GetChildren() }
for i = 1, table.getn(kids) do
if _looksLikeJoin(kids[i]) then return kids[i] end
end
-- One level deeper.
for i = 1, table.getn(kids) do
local k = kids[i]
if k and k.GetChildren then
local gkids = { k:GetChildren() }
for j = 1, table.getn(gkids) do
if _looksLikeJoin(gkids[j]) then return gkids[j] end
end
end
end
end
return nil
end
-- Global sweep as a last resort: scan all frames for a visible Join-style
-- button attached to a visible top-level frame.
local function GlobalFindArenaJoinButton()
local f = EnumerateFrames()
while f do
if f.IsVisible and f:IsVisible() and f.GetChildren then
local kids = { f:GetChildren() }
for i = 1, table.getn(kids) do
if _looksLikeJoin(kids[i]) then return kids[i] end
end
end
f = EnumerateFrames(f)
end
return nil
end
local arenaPoll
local arenaPollElapsed = 0
local arenaPollDeadline = 0
local function StopArenaPoll()
if arenaPoll then arenaPoll:Hide() end
end
local function StartArenaPoll()
if not arenaPoll then
arenaPoll = CreateFrame("Frame")
arenaPoll:Hide()
arenaPoll:SetScript("OnUpdate", function()
arenaPollElapsed = arenaPollElapsed + arg1
if arenaPollElapsed < 0.05 then return end
arenaPollElapsed = 0
local frame = FindArenaFinderFrame()
local btn = FindJoinButtonIn(frame)
if not btn then btn = GlobalFindArenaJoinButton() end
if btn then
-- Click the button as if the user pressed it.
if btn.Click then btn:Click() end
if frame and frame.Hide then frame:Hide() end
HidePvPPanels()
StopArenaPoll()
return
end
if GetTime() > arenaPollDeadline then
-- Give up quietly; leave the panel open so the user can click.
StopArenaPoll()
end
end)
end
arenaPollElapsed = 0
arenaPollDeadline = GetTime() + 3.0
arenaPoll:Show()
end
function RelationshipsPVP_QueueArena(key)
local meta = ARENA_META[key]
if not meta then return end
local asGroup = IsInGroup()
lastQueuedArena = key
-- 1) Direct TBC-style API — no panel involved.
if type(JoinArena) == "function" then
if type(SetArenaTeamSize) == "function" then SetArenaTeamSize(meta.size) end
JoinArena(meta.size, meta.rated, asGroup)
Print("Queued arena: |cffffd200" .. BG_NAMES[key] .. "|r"
.. (asGroup == 1 and " |cff66ff66(group)|r" or ""))
return
end
-- 2) Turtle/Octo custom entry point. JoinArenaQueue(id) opens the
-- Blood Ring finder; we then click its Join button ourselves.
if type(JoinArenaQueue) == "function" then
local id = ARENA_IDS[key]; if id == nil then id = 0 end
if type(SetArenaTeamSize) == "function" then SetArenaTeamSize(meta.size) end
JoinArenaQueue(id)
StartArenaPoll()
Print("Queued arena: |cffffd200" .. BG_NAMES[key] .. "|r"
.. (asGroup == 1 and " |cff66ff66(group)|r" or ""))
return
end
Print("|cffff4444Arena API not available on this client. Visit an Arena Battlemaster.|r")
end
---------------------------------------------------------------
-- Queue for all standard battlegrounds (no arenas, no rated)
---------------------------------------------------------------
function RelationshipsPVP_QueueAllBGs()
local asGroup = IsInGroup()
local kicked = 0
for i = 1, table.getn(BG_KEYS) do
local key = BG_KEYS[i]
if EnqueueAndKick({ kind = "bg", key = key, asGroup = asGroup }) then
kicked = kicked + 1
end
end
if kicked > 0 then
Print("Queueing all standard battlegrounds ("
.. kicked .. "): |cffffd200AV, WSG, AB|r"
.. (asGroup == 1 and " |cff66ff66(group)|r" or ""))
else
Print("|cffff4444Battleground Finder not available on this client.|r")
end
end
function RelationshipsPVP_LeaveAll()
local left = 0
for i = 1, 3 do
local status = GetBattlefieldStatus(i)
if status == "queued" or status == "confirm" then
AcceptBattlefieldPort(i, 0)
left = left + 1
end
end
if left > 0 then
Print("Left " .. left .. " pending queue(s).")
else
Print("No active queues.")
end
pendingQueue = {}
lastQueuedArena = nil
end
---------------------------------------------------------------
-- Minimap icon toggle
---------------------------------------------------------------
function RelationshipsPVP_ToggleMinimapIcon()
local db = EnsureDB()
db.showIcon = not db.showIcon
ApplyIconVisibility()
Print(db.showIcon and "Minimap icon |cff66ff66shown|r." or "Minimap icon |cffff6666hidden|r. Use /rpvp to open.")
end
---------------------------------------------------------------
-- UI: build row FontStrings inside the stats panel
---------------------------------------------------------------
local COL_X = { 14, 158, 208, 258, 308, 358, 408, 454, 500, 550, 604, 668 }
local COL_W = { 140, 46, 46, 46, 46, 46, 46, 46, 46, 50, 60, 58 }
local COL_J = { "LEFT", "CENTER", "CENTER", "CENTER", "CENTER", "CENTER", "CENTER", "CENTER", "CENTER", "CENTER", "CENTER", "CENTER" }
local COL_NAMES = { "bg", "games", "win", "draw", "loss", "ratio", "kb", "hk", "dth", "kd", "honor", "mk" }
local ROW_HEIGHT = 22
local ROW_TOP = -34
local function MakeRow(panel, tag, yOff, striped)
local row = {}
if striped then
local bg = panel:CreateTexture("RPVPRowBG"..tag, "BACKGROUND")
bg:SetTexture(1, 1, 1, 0.04)
bg:SetWidth(714); bg:SetHeight(ROW_HEIGHT - 2)
bg:SetPoint("TOPLEFT", panel, "TOPLEFT", 14, yOff + 2)
end
for c = 1, table.getn(COL_NAMES) do
local fs = panel:CreateFontString(
"RPVPRow"..tag.."_"..COL_NAMES[c],
"ARTWORK",
(c == 1) and "GameFontNormal" or "GameFontHighlight"
)
fs:SetWidth(COL_W[c]); fs:SetHeight(18)
fs:SetJustifyH(COL_J[c])
fs:SetPoint("TOPLEFT", panel, "TOPLEFT", COL_X[c], yOff)
row[COL_NAMES[c]] = fs
end
return row
end
local function BuildStatsRows()
if rowStrings["_built"] then return end
local panel = RelationshipsPVPStatsPanel
if not panel then return end
for i = 1, table.getn(ALL_KEYS) do
local key = ALL_KEYS[i]
local yOff = ROW_TOP - (i - 1) * ROW_HEIGHT
local row = MakeRow(panel, key, yOff, (math.mod(i, 2) == 0))
local col = BG_COLORS[key]
row.bg:SetText(BG_NAMES[key])
row.bg:SetTextColor(col.r, col.g, col.b)
rowStrings[key] = row
end
local n = table.getn(ALL_KEYS)
local totalY = ROW_TOP - n * ROW_HEIGHT - 4
local div = panel:CreateTexture("RPVPTotalDivider", "ARTWORK")
div:SetTexture(1, 1, 1, 0.25)
div:SetWidth(714); div:SetHeight(1)
div:SetPoint("TOPLEFT", panel, "TOPLEFT", 14, totalY + 2)
totalRow = MakeRow(panel, "TOTAL", totalY - 6, false)
totalRow.bg:SetText("Total")
totalRow.bg:SetTextColor(1.0, 0.95, 0.7)
rowStrings["_built"] = true
end
local function FormatQueueLine()
local lines = ""
local any = false
for i = 1, 3 do
local status, mapName, _, _, _, teamSize, registered = GetBattlefieldStatus(i)
if status and status ~= "none" then
any = true
local color = "|cffffffff"
if status == "queued" then color = "|cffffff66" end
if status == "confirm" then color = "|cff66ff66" end
if status == "active" then color = "|cff66ccff" end
local label = mapName or "?"
if teamSize and teamSize > 0 then
label = label .. " (" .. teamSize .. "v" .. teamSize
.. ((registered == 1 or registered == true) and " rated" or " skirmish") .. ")"
end
lines = lines .. " [" .. i .. "] " .. label
.. " - " .. color .. status .. "|r\n"
end
end
if not any then return " |cff888888(no active queues)|r\n" end
return lines
end
local function FillRow(r, s)
local draws = s.draws or 0
local games = s.wins + draws + s.losses
local pct = 0
if games > 0 then pct = math.floor((s.wins / games) * 100 + 0.5) end
local kd = "-"
if s.deaths > 0 then
kd = string.format("%.2f", (s.kills + s.hk) / s.deaths)
elseif (s.kills + s.hk) > 0 then
kd = "\226\136\158"
end
r.games:SetText(games)
r.win:SetText(s.wins)
r.draw:SetText(draws)
r.loss:SetText(s.losses)
r.ratio:SetText(pct .. "%")
r.kb:SetText(s.kills)
r.hk:SetText(s.hk)
r.dth:SetText(s.deaths)
r.kd:SetText(kd)
r.honor:SetText(FormatNumber(s.honor))
r.mk:SetText(s.marks)
if games == 0 then
r.ratio:SetTextColor(0.7, 0.7, 0.7)
elseif pct >= 60 then
r.ratio:SetTextColor(0.4, 1.0, 0.4)
elseif pct >= 40 then
r.ratio:SetTextColor(1.0, 0.9, 0.4)
else
r.ratio:SetTextColor(1.0, 0.5, 0.5)
end
end
function RelationshipsPVP_Refresh()
if not RelationshipsPVPFrame or not RelationshipsPVPFrame:IsVisible() then return end
local db = EnsureDB()
BuildStatsRows()
local totals = NewStatBlock()
for i = 1, table.getn(ALL_KEYS) do
local k = ALL_KEYS[i]
local s = db.stats[k]
local r = rowStrings[k]
if r then FillRow(r, s) end
totals.wins = totals.wins + s.wins
totals.draws = totals.draws + (s.draws or 0)
totals.losses = totals.losses + s.losses
totals.kills = totals.kills + s.kills
totals.hk = totals.hk + s.hk
totals.deaths = totals.deaths + s.deaths
totals.honor = totals.honor + s.honor
totals.marks = totals.marks + s.marks
if s.bestStreak > totals.bestStreak then totals.bestStreak = s.bestStreak end
if s.bestKB > totals.bestKB then totals.bestKB = s.bestKB end
end
if totalRow then FillRow(totalRow, totals) end
if RelationshipsPVPExtrasText then
RelationshipsPVPExtrasText:SetText(
"|cffffd200Best win streak:|r " .. totals.bestStreak
.. " |cffffd200Best KBs (single match):|r " .. totals.bestKB
.. " |cffffd200Total honor:|r " .. FormatNumber(totals.honor)
)
end
local qtext = FormatQueueLine()
if currentBG then
qtext = qtext .. "\n |cff66ff66Currently inside: " .. BG_NAMES[currentBG] .. "|r"
end
if RelationshipsPVPQueueText then
RelationshipsPVPQueueText:SetText(qtext)
end
end
---------------------------------------------------------------
-- Reset panel + minimap button to the centre of the screen
---------------------------------------------------------------
function RelationshipsPVP_ResetPositions()
local db = EnsureDB()
if RelationshipsPVPFrame then
RelationshipsPVPFrame:ClearAllPoints()
RelationshipsPVPFrame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
RelationshipsPVPFrame:Show()
end
if RelationshipsPVPMinimapButton then
RelationshipsPVP_SetIconPosition(0, 0, 1)
db.icon.point = "CENTER"
db.icon.relPoint = "CENTER"
db.icon.x = 0
db.icon.y = 0
db.icon.parent = "UIParent"
end
db.panel = { point = "CENTER", x = 0, y = 0 }
Print("Panel and minimap button reset to screen centre.")
end
function RelationshipsPVP_ResetStats()
local db = EnsureDB()
for i = 1, table.getn(ALL_KEYS) do
db.stats[ALL_KEYS[i]] = NewStatBlock()
end
Print("PvP statistics cleared.")
RelationshipsPVP_Refresh()
end
---------------------------------------------------------------
-- Events
---------------------------------------------------------------
function RelationshipsPVP_OnLoad()
this:RegisterEvent("PLAYER_LOGIN")
this:RegisterEvent("PLAYER_ENTERING_WORLD")
this:RegisterEvent("PLAYER_LOGOUT")
this:RegisterEvent("UPDATE_BATTLEFIELD_STATUS")
this:RegisterEvent("UPDATE_BATTLEFIELD_SCORE")
this:RegisterEvent("PLAYER_DEAD")
this:RegisterEvent("PLAYER_REGEN_ENABLED")
this:RegisterEvent("CHAT_MSG_BG_SYSTEM_NEUTRAL")
this:RegisterEvent("CHAT_MSG_BG_SYSTEM_ALLIANCE")
this:RegisterEvent("CHAT_MSG_BG_SYSTEM_HORDE")
SLASH_RELATIONSHIPSPVP1 = "/rpvp"
SLASH_RELATIONSHIPSPVP2 = "/relpvp"
SLASH_RELATIONSHIPSPVP3 = "/relationshipspvp"
SlashCmdList["RELATIONSHIPSPVP"] = function(msg)
msg = string.lower(msg or "")
if msg == "leave" then
RelationshipsPVP_LeaveAll()
elseif msg == "av" then
RelationshipsPVP_Queue("AV")
elseif msg == "wsg" then
RelationshipsPVP_Queue("WSG")
elseif msg == "ab" then
RelationshipsPVP_Queue("AB")
elseif msg == "all" then
RelationshipsPVP_QueueAllBGs()
elseif msg == "2v2" then
RelationshipsPVP_QueueArena("A2")
elseif msg == "3v3" then
RelationshipsPVP_QueueArena("A3")
elseif msg == "5v5" then
RelationshipsPVP_QueueArena("A5")
elseif msg == "skirmish" or msg == "sk" then
RelationshipsPVP_QueueArena("SK")
elseif msg == "icon" or msg == "minimap" then
RelationshipsPVP_ToggleMinimapIcon()
elseif msg == "reset" then
RelationshipsPVP_ResetPositions()
elseif msg == "clear" or msg == "wipe" then
RelationshipsPVP_ResetStats()
else
if RelationshipsPVPFrame:IsVisible() then
RelationshipsPVPFrame:Hide()
else
RelationshipsPVPFrame:Show()
end
end
end
Print("Loaded v1.8. Type /rpvp to open, /rpvp all to queue all BGs, /rpvp icon to toggle minimap icon.")
end
function RelationshipsPVP_SavePanelPos()
SavePanelPos()
end
function RelationshipsPVP_OnEvent(event)
if event == "PLAYER_LOGOUT" then
RelationshipsPVP_SaveIconPos()
SavePanelPos()
return
end
if event == "PLAYER_LOGIN" or event == "PLAYER_ENTERING_WORLD" then
EnsureDB()
RestoreIconPos()
RestorePanelPos()
local key = DetectCurrentBG()
currentBG = key
if not key then endReported = {} end
RelationshipsPVP_Refresh()
elseif event == "UPDATE_BATTLEFIELD_STATUS" then
local key = DetectCurrentBG()
currentBG = key
RelationshipsPVP_Refresh()
elseif event == "UPDATE_BATTLEFIELD_SCORE" then
local key, idx = DetectCurrentBG()
if key and idx and GetBattlefieldWinner() then
RecordEnd(key, idx)
end
RelationshipsPVP_Refresh()
elseif event == "PLAYER_REGEN_ENABLED" then
RelationshipsPVP_Refresh()
elseif event == "CHAT_MSG_BG_SYSTEM_NEUTRAL"
or event == "CHAT_MSG_BG_SYSTEM_ALLIANCE"
or event == "CHAT_MSG_BG_SYSTEM_HORDE" then
RequestBattlefieldScoreData()
end
end