1109 lines
35 KiB
Lua
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
|