Files
Adrestie ca2b78e217 Remove every debug command
/fui keeps edit mode, reset and list. The thirty diagnostic and tuning
functions behind the other subcommands (bar, bags, character, tabs, close,
model, minimap, mouse, map, questlog, chat, chatlines, tabard...) go with
them, with Panes.Report, the bags read-back and the 205 texts they alone used.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 13:39:35 +02:00

829 lines
27 KiB
Lua

-- ForeverUI: PvP tab of the character sheet (camelot PVPRankFrame.xml, pvprankframe.lua).
-- Left pane: rank title, honor stats, rank gauge and arena teams. Right pane: battlegrounds.
local ForeverUI = ForeverUI or {}
_G.ForeverUI = ForeverUI
local L = ForeverUI.L
local SEASON_X, SEASON_Y = -46, -18
-- Left pane layout: rank title at the top, honor on two columns under the title line, then a
-- separator, the dial and the win counter.
-- Measured on image alpha (threshold 128, shadow ignored):
-- title line ui-character-info-honor-levelbg, 350 x 62, solid on rows 53-58: it ends
-- 4 px above its image bottom
-- ring solid from row 31 of 201 (drawn at 209): it shows 6 px below the dial top
local BLOCK_Y = 0
local RANK_TOP = -12
local HONOR_BELOW_LINE = -2 -- 6 px below the solid line
local HONOR_W = 366
local HONOR_COLUMN = 173 -- (366 - 20) / 2
local HONOR_ROW_H, HONOR_GAP = 14, 3
local HONOR_SEPARATOR_Y = -4
local DIAL_BELOW_SEPARATOR = 0 -- the ring shows 6 px lower
local BLOCK_Y2 = -195
local SEASON_TITLE_Y = -20
-- Win counter, 5 px below the visible bottom of the gauge. The ring (230 x 201, drawn at
-- 235 x 209) is solid down to row 169, the rest is a fading shadow: its solid bottom is 5 px
-- above the dial bottom; the reward ring (54, solid down to row 49, at -1) is 4 px above.
-- Fixed height: it is empty without a rank, and what follows must not move up.
local PROGRESS_BELOW_DIAL = -1
local PROGRESS_H = 12
local LINE_Y = -10
local DIAL_SIZE = 154
local GLOW_W, GLOW_H = 275, 295
local BACKGROUND_W, BACKGROUND_H = 115, 115
local RING_W, RING_H = 235, 209
local RING_Y = -1
local BADGE_W, BADGE_H = 72, 84
local REWARD_Y = -1
local LINE_ATLAS = "ui-character-info-honor-levelbg"
local GLOW_ATLAS = "ui-character-info-honor-bar-bg-glow"
local RING_ATLAS = "ui-character-info-honor-bar-bg"
local ATLAS_BACKGROUND = "ui-character-info-honor-bar-bg-%s"
local FACTION_BADGE_ATLAS = "ui-character-info-honor-icon-%s"
local RANK_BADGE_ATLAS = "ui-character-info-honor-icon-%d"
-- Dishonored: the broken faction crest (tools/bake_dishonor.py) replaces the faction badge.
local BROKEN_CREST = "Interface\\ForeverUI\\pvpframe\\honor-dishonored-%s"
local REWARD_RING_ATLAS = "ui-character-info-honor-rewardring"
local LONG_SEPARATOR_ATLAS = "ui-character-info-scrollline-long"
-- Gauge. camelot uses a Cooldown with a custom swipe texture; 3.3.5 has no SetSwipeTexture and
-- the Cooldown swipe is shared by the whole game. Each dial quarter shows either the full ring
-- or a rotated half ring cut by the quarter rectangle (both baked by tools/bake_masks.py).
local GAUGE_FULL = "Interface\\ForeverUI\\PvP\\honorfill"
local GAUGE_HALF = "Interface\\ForeverUI\\PvP\\honorfillhalf"
local GAUGE_ORIGIN = 180 -- <Cooldown rotation="180">: starts at six o'clock
local GAUGE_DIRECTION = 1 -- 1: clockwise; -1: counterclockwise
local PANE_W = 398
local block, detail
-- Forward declarations: updateBlock, defined above, calls clampToPane; without them the names
-- would resolve to globals (nil).
local leftHost
local clampToPane
-- --------------------------------------------------------------- Data
local function faction()
return (UnitFactionGroup and UnitFactionGroup("player")) or "Alliance"
end
-- Rank names come from the client strings PVP_RANK_<index>_<faction01> (camelot
-- GetPVPRankText). Indices 1-4 are the negative ranks (Pariah, Outlaw, Exiled, Dishonored);
-- rank N is index N + 4. GetPVPRankInfo returns nothing on this server.
-- A player without rank is a civilian, not dishonored. "Civilian" is not a client string, so
-- it comes from the text table.
local UNRANKED_NAME = L.PVPTAB_CIVILIAN
local function rankName(index)
if not index or index <= 0 then
return nil
end
local faction01 = (faction() == "Alliance") and 1 or 0
return _G["PVP_RANK_" .. tostring(index) .. "_" .. tostring(faction01)]
end
-- The rank comes from titles: mod-pvp-titles (AwardEarnedTitles) sets a CharTitles title for
-- each lifetime kill threshold and never sets the old rank counter, so UnitPVPRank stays 0.
-- Title ids (CharTitles.dbc), in rank order: 1-14 Alliance, 15-28 Horde. For these ids the bit
-- index equals the id, so IsTitleKnown takes them as is.
local FIRST_TITLE = { Alliance = 1, Horde = 15 }
local RANK_COUNT = 14
-- IsTitleKnown returns a number, and 0 is true in Lua: compare with 0, as
-- PaperDollFrame.lua does (IsTitleKnown(i) ~= 0).
local function isTitleKnown(identifier)
if not IsTitleKnown then
return false
end
local known = IsTitleKnown(identifier)
return known ~= nil and known ~= false and known ~= 0
end
local function rankByTitles()
local first = FIRST_TITLE[faction()] or FIRST_TITLE.Alliance
for number = RANK_COUNT, 1, -1 do
if isTitleKnown(first + number - 1) then
return number
end
end
return 0
end
-- Lifetime kills needed for each rank are server settings (mod_pvptitles.conf,
-- PvPTitles.Rank_1 to Rank_14) that no client API returns. mod-pvp-titles-ext whispers them on
-- the PVPTITLES addon prefix ("RANKS:k1,...,k14") at login and on "REQ".
-- Without a reply they stay nil: the tab shows the rank and kill count only, with no
-- threshold or gauge; inspection shows the faction badge without rank name.
local THRESHOLDS = nil
-- Dishonored state (mod-pvp-titles-ext "DISHONOR:..."): finish is its end in GetTime time,
-- nil outside the state. Shown as the negative rank Dishonored (PVP_RANK_4), with the time
-- left instead of the progress.
local DISHONOR = { finish = nil }
-- Shared with Buffs.lua, which shows it as a debuff; name() is the client's negative rank
-- string, in the client language.
DISHONOR.name = function() return rankName(4) or "" end
ForeverUI.Dishonor = DISHONOR
local function updateDebuffs()
if ForeverUI.Buffs and ForeverUI.Buffs.update then
ForeverUI.Buffs.update()
end
end
-- Gauge fraction: lifetime wins between the threshold of rank number and the next one.
local function progressByWins(number, wins)
if not THRESHOLDS then
return 0 -- unknown thresholds: no gauge
end
local following = THRESHOLDS[number + 1]
if not following then
return 1 -- max rank: full gauge
end
local earned = (number > 0) and THRESHOLDS[number] or 0
if following <= earned then
return 0
end
local part = ((wins or 0) - earned) / (following - earned)
if part < 0 then
return 0
elseif part > 1 then
return 1
end
return part
end
-- Rank of another player, for Inspect.lua. Titles cannot be read for other units, but lifetime
-- kills can (GetInspectHonorData): the rank is the number of thresholds reached, as the server
-- module awards them. Unknown thresholds: nil. fac: "Alliance" or "Horde".
ForeverUI.PvPRanks = {
byWins = function(wins)
if not THRESHOLDS then
return nil
end
local number = 0
for i, threshold in ipairs(THRESHOLDS) do
if (wins or 0) >= threshold then number = i end
end
return number
end,
name = function(number, fac)
if not number or number <= 0 then return UNRANKED_NAME end
local faction01 = (fac == "Alliance") and 1 or 0
return _G["PVP_RANK_" .. tostring(number + 4) .. "_" .. tostring(faction01)]
end,
badgeRank = RANK_BADGE_ATLAS,
badgeFaction = FACTION_BADGE_ATLAS,
}
-- Player rank: counter index, name, number, gauge progress, lifetime wins and next threshold.
local function readRank()
local index = UnitPVPRank and UnitPVPRank("player") or 0
local name, number
if index and index > 0 and GetPVPRankInfo then
name, number = GetPVPRankInfo(index, "player")
end
if not number or number <= 0 then
number = (index > 4) and (index - 4) or 0
end
-- The server never sets the rank counter: use the titles.
if number <= 0 then
number = rankByTitles()
end
-- Name, three cases:
-- number > 0 real rank: index = number + 4
-- index 1 to 4 negative rank (Pariah, Outlaw, Exiled, Dishonored), named by its index
-- otherwise no rank: civilian
if not name then
if number > 0 then
name = rankName(number + 4)
elseif index and index >= 1 and index <= 4 then
name = rankName(index)
else
name = UNRANKED_NAME
end
end
local wins = 0
if GetPVPLifetimeStats then
wins = GetPVPLifetimeStats() or 0
end
local progress = 0
if GetPVPRankProgress then
progress = GetPVPRankProgress() or 0
end
if not progress or progress <= 0 then
progress = progressByWins(number, wins)
end
-- Dishonored: negative rank, no gauge, time left.
local rest = DISHONOR.finish and (DISHONOR.finish - GetTime())
if rest and rest > 0 then
return {
index = 4,
name = rankName(4),
number = 0,
progress = 0,
wins = wins,
dishonored = rest,
}
end
return {
index = index,
name = name,
number = number,
progress = progress,
wins = wins,
threshold = THRESHOLDS and THRESHOLDS[number + 1],
}
end
-- Honor stats, still provided by 3.3.5.
local function readHonor()
local lifetime, dishonor, highestRank = 0, 0, 0
if GetPVPLifetimeStats then
lifetime, dishonor, highestRank = GetPVPLifetimeStats()
end
local day, todayPoints = 0, 0
if GetPVPSessionStats then
day, todayPoints = GetPVPSessionStats()
end
local yesterday, yesterdayPoints = 0, 0
if GetPVPYesterdayStats then
yesterday, yesterdayPoints = GetPVPYesterdayStats()
end
local current = GetHonorCurrency and GetHonorCurrency() or 0
return {
lifetime = lifetime or 0,
dishonor = dishonor or 0,
highestRank = highestRank or 0,
day = day or 0,
todayPoints = todayPoints or 0,
yesterday = yesterday or 0,
yesterdayPoints = yesterdayPoints or 0,
current = current or 0,
}
end
-- ----------------------------------------------------------------- Gauge
-- Dial quarters {u1, u2, v1, v2}, clockwise from twelve o'clock, as fractions of the dial:
-- u to the right, v down (texture coordinates).
local QUADRANTS = {
{ 0.5, 1.0, 0.0, 0.5 }, -- top right: 0 to 90 degrees
{ 0.5, 1.0, 0.5, 1.0 }, -- bottom right: 90 to 180
{ 0.0, 0.5, 0.5, 1.0 }, -- bottom left: 180 to 270
{ 0.0, 0.5, 0.0, 0.5 }, -- top left: 270 to 360
}
-- Where to read dial point (u, v) in a texture rotated clockwise by the angle (cosine, sine).
-- 8-argument SetTexCoord does not move the corners on screen, it picks the image point each
-- one shows: rotating the image means rotating the lookup the other way around the center.
-- Points outside [0, 1] repeat the empty edge texel, so the corners stay transparent.
local function rotate(u, v, cosine, sine)
local du, dv = u - 0.5, v - 0.5
return 0.5 + du * cosine + dv * sine, 0.5 - du * sine + dv * cosine
end
-- Anchors a quarter texture on the dial; mirrored when the gauge runs counterclockwise.
local function placeQuarter(tex, dial, u1, u2, v1, v2)
if GAUGE_DIRECTION < 0 then
u1, u2 = 1 - u2, 1 - u1
end
tex:ClearAllPoints()
tex:SetPoint("TOPLEFT", dial, "TOPLEFT", u1 * DIAL_SIZE, -v1 * DIAL_SIZE)
tex:SetPoint("BOTTOMRIGHT", dial, "TOPLEFT", u2 * DIAL_SIZE, -v2 * DIAL_SIZE)
end
-- Creates the four quarters of a dial, each with a full-ring texture and a half-ring arc.
local function buildGauge(dial)
local quarters = {}
for i = 1, 4 do
local quarter = QUADRANTS[i]
local u1, u2, v1, v2 = quarter[1], quarter[2], quarter[3], quarter[4]
local q = { u1 = u1, u2 = u2, v1 = v1, v2 = v2 }
-- Angle where this quarter starts, measured from the gauge origin.
q.origin = (90 * (i - 1) - GAUGE_ORIGIN) % 360
-- Passed quarter: the full ring, read on this quarter.
q.full = dial:CreateTexture(nil, "ARTWORK")
q.full:SetTexture(GAUGE_FULL)
q.full:SetTexCoord(u1, u2, v1, v2)
placeQuarter(q.full, dial, u1, u2, v1, v2)
q.full:Hide()
-- Quarter where the gauge stops: the rotated half ring.
q.arc = dial:CreateTexture(nil, "ARTWORK")
q.arc:SetTexture(GAUGE_HALF)
placeQuarter(q.arc, dial, u1, u2, v1, v2)
q.arc:Hide()
quarters[i] = q
end
return quarters
end
-- Shows the quarters of a gauge (buildGauge) for a fraction from 0 to 1.
local function updateQuarters(quarters, fraction)
fraction = fraction or 0
if fraction < 0 then
fraction = 0
elseif fraction > 1 then
fraction = 1
end
local traveled = fraction * 360
for i = 1, 4 do
local q = quarters[i]
if traveled >= q.origin + 90 then
q.arc:Hide()
q.full:Show()
elseif traveled <= q.origin then
q.arc:Hide()
q.full:Hide()
else
q.full:Hide()
-- The half ring covers the 180 degrees that end at its rotation angle. Rotate it so they end
-- at the gauge head (GAUGE_ORIGIN + traveled), minus the half turn it already has.
local phi = math.rad(GAUGE_ORIGIN + traveled - 360)
local cosine, sine = math.cos(phi), math.sin(phi)
local tlu, tlv = rotate(q.u1, q.v1, cosine, sine)
local blu, blv = rotate(q.u1, q.v2, cosine, sine)
local tru, trv = rotate(q.u2, q.v1, cosine, sine)
local bru, brv = rotate(q.u2, q.v2, cosine, sine)
q.arc:SetTexCoord(tlu, tlv, blu, blv, tru, trv, bru, brv)
q.arc:Show()
end
end
end
local function updateGauge(fraction)
if not block or not block.gauge then
return
end
updateQuarters(block.gauge, fraction)
end
ForeverUI.PvPGauge = updateGauge
-- --------------------------------------------------------------- Display
local function placeBadge(rank)
if not block then
return
end
-- Dishonored: the broken faction crest, drawn on the badge silhouette so it blends into the
-- dial like the badge.
if rank.dishonored then
block.badge:SetTexture(string.format(BROKEN_CREST, string.lower(faction())))
block.badge:SetTexCoord(0, 1, 0, 1)
block.badge:SetWidth(BADGE_W)
block.badge:SetHeight(BADGE_H)
return
end
-- camelot UpdateFactionBadge: faction badge without rank, rank badge otherwise.
local installed = false
if rank.number and rank.number > 0 then
installed = ForeverUI.SetAtlas(block.badge,
string.format(RANK_BADGE_ATLAS, rank.number), true)
end
if not installed then
ForeverUI.SetAtlas(block.badge,
string.format(FACTION_BADGE_ATLAS, string.lower(faction())), true)
end
block.badge:SetWidth(BADGE_W)
block.badge:SetHeight(BADGE_H)
end
local function updateBlock()
if not block then
return
end
clampToPane()
local rank = readRank()
-- No season line: the arena teams use its space.
block.season:SetText("")
block.season:Hide()
-- Rank title at the top; the rank number only in the reward ring (camelot also writes
-- PVP_RANK_NUMBER_AND_TITLE at the top).
block.rank:SetText(rank.name or "")
-- Honor labels from the client GlobalStrings, as WotLK PVPHonor: HONOR_POINTS,
-- HONORABLE_KILLS, HONOR_TODAY, HONOR_YESTERDAY. LIFETIME_HONORABLE_KILLS, TODAY and
-- YESTERDAY do not exist in 3.3.5.
local honor = readHonor()
local cells = {
{ HONOR_POINTS, honor.current },
{ HONORABLE_KILLS, honor.lifetime },
{ HONOR_TODAY,
tostring(honor.day) .. " (" .. tostring(honor.todayPoints) .. ")" },
{ HONOR_YESTERDAY,
tostring(honor.yesterday) .. " (" .. tostring(honor.yesterdayPoints) .. ")" },
}
for n, c in ipairs(cells) do
block.honor.cells[n].label:SetText(c[1])
block.honor.cells[n].value:SetText(tostring(c[2]))
end
placeBadge(rank)
updateGauge(rank.progress)
-- Progress as numbers, like camelot CurrentRankProgressField (PVP_RANK_CURRENT_PROGRESS does
-- not exist in 3.3.5): lifetime kills / next threshold, the values mod-pvp-titles compares.
-- At max rank or with unknown thresholds, the kill count alone; dishonored, the time left.
if rank.dishonored then
block.progress:SetText(string.format(L.PVPTAB_DISHONORED_LEFT,
SecondsToTime(rank.dishonored)))
elseif rank.threshold then
block.progress:SetText(string.format("%d / %d", rank.wins or 0,
rank.threshold))
else
block.progress:SetText(tostring(rank.wins or 0))
end
-- No rank, no reward ring: an empty gold ring looks like a bug. The dial keeps its glow,
-- faction background and badge.
if rank.number and rank.number > 0 then
block.number:SetText(tostring(rank.number))
block.reward:Show()
block.number:Show()
else
block.number:SetText("")
block.reward:Hide()
block.number:Hide()
end
-- Arena teams below the rank (PvPArena.lua).
if ForeverUI.PvPArena then
ForeverUI.PvPArena.update()
end
end
ForeverUI.PvPUpdate = updateBlock
-- ---------------------------------------------------------- Build
-- Hides the whole client PvP screen on every pass: PVPParentFrame regions and child frames
-- (tabs, arena team frames, off-season banner and their art).
local function suppressClientScreen()
local frame = _G["PVPParentFrame"]
if not frame then
return
end
if frame.SetBackdrop then
frame:SetBackdrop(nil)
end
for _, region in ipairs({ frame:GetRegions() }) do
if region.Hide then
region:Hide()
end
end
if frame.GetChildren then
for _, childFrame in ipairs({ frame:GetChildren() }) do
if childFrame ~= block and childFrame.Hide then
childFrame:Hide()
end
end
end
end
-- PVPParentFrame is registered in UIPanelWindows (UIParent.lua). The panel system then
-- re-anchors it to UIParent (UpdateUIPanelPositions) as an invisible 384 x 512 frame that
-- takes the mouse clicks of everything below it, such as the side tabs. Removing it from the
-- table makes it plain pane content.
local function detachFromPanelSystem(frame)
-- If the panel system already manages it, it keeps its slot until HideUIPanel: released
-- while the frame still counts as a panel.
if HideUIPanel and frame.IsShown and frame:IsShown() then
HideUIPanel(frame)
end
if UIPanelWindows then
UIPanelWindows["PVPParentFrame"] = nil
end
-- GetUIPanelWindowInfo (UIParent.lua) copies the table entry into the frame's UIPanelLayout-*
-- attributes on first use (a TogglePVPFrame before this build) and then reads only them:
-- cleared too, as PaperDollFrame.lua does for CharacterFrame.
frame:SetAttribute("UIPanelLayout-defined", nil)
frame:SetAttribute("UIPanelLayout-enabled", nil)
end
-- Pins the frame to the pane on every pass, in case another system re-anchors it.
clampToPane = function()
local frame = _G["PVPParentFrame"]
local host = leftHost
if not frame or not host then
return
end
if frame:GetParent() ~= host then
frame:SetParent(host)
end
if frame.SetToplevel then
frame:SetToplevel(false)
end
frame:ClearAllPoints()
frame:SetPoint("TOPLEFT", host, "TOPLEFT", 0, 0)
frame:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", 0, 0)
end
local function build(host)
local frame = _G["PVPParentFrame"]
if not frame or not host then
return nil, {}
end
leftHost = host
detachFromPanelSystem(frame)
clampToPane()
if block then
suppressClientScreen()
updateBlock()
return nil, { frame }
end
local width = host:GetWidth() or 0
if width < 100 then
width = PANE_W
end
-- Main block: from the pane top (BLOCK_Y) down to BLOCK_Y2.
block = CreateFrame("Frame", "ForeverUIPvPMain", frame)
block:SetPoint("TOPLEFT", host, "TOPLEFT", 0, BLOCK_Y)
block:SetPoint("BOTTOMRIGHT", host, "TOPRIGHT", 0, BLOCK_Y2)
-- Season end countdown: the line exists but stays empty (no C_SeasonInfo in 3.3.5).
block.timer = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
block.timer:SetJustifyH("RIGHT")
block.timer:SetPoint("TOPRIGHT", host, "TOPRIGHT", SEASON_X, SEASON_Y)
-- GameFontNormalMed2 does not exist in 3.3.5.
block.season = block:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
block.season:SetJustifyH("CENTER")
block.season:SetPoint("TOP", block, "TOP", 0, SEASON_TITLE_Y)
block.rank = block:CreateFontString(nil, "ARTWORK", "GameFontHighlightLarge")
block.rank:SetJustifyH("CENTER")
block.rank:SetPoint("TOP", block, "TOP", 0, RANK_TOP)
block.row = block:CreateTexture(nil, "ARTWORK")
ForeverUI.SetAtlas(block.row, LINE_ATLAS)
block.row:SetPoint("BOTTOM", block.rank, "BOTTOM", 0, LINE_Y)
block.progress = block:CreateFontString(nil, "ARTWORK", "GameFontNormal")
block.progress:SetJustifyH("CENTER")
block.progress:SetHeight(PROGRESS_H)
-- Dial: glow, faction background, ring, gauge and badge, in this draw order.
local dial = CreateFrame("Frame", "ForeverUIPvPDial", block)
dial:SetWidth(DIAL_SIZE)
dial:SetHeight(DIAL_SIZE)
dial:SetFrameLevel(block:GetFrameLevel() + 1)
block.dial = dial
-- Honor on two columns, in a frame above the dial: the dial glow spreads upward and would
-- veil the text.
local honor = CreateFrame("Frame", "ForeverUIPvPHonor", block)
honor:SetWidth(HONOR_W)
honor:SetHeight(2 * HONOR_ROW_H + HONOR_GAP)
honor:SetPoint("TOP", block.row, "BOTTOM", 0, HONOR_BELOW_LINE)
honor:SetFrameLevel(block:GetFrameLevel() + 2)
block.honor = honor
honor.cells = {}
for n = 1, 4 do
local column, rowLine = (n - 1) % 2, math.floor((n - 1) / 2)
local x = column * (HONOR_W - HONOR_COLUMN)
local y = -rowLine * (HONOR_ROW_H + HONOR_GAP)
local label = honor:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
label:SetJustifyH("LEFT")
label:SetPoint("TOPLEFT", honor, "TOPLEFT", x, y)
local value = honor:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
value:SetJustifyH("RIGHT")
value:SetPoint("TOPRIGHT", honor, "TOPLEFT", x + HONOR_COLUMN, y)
honor.cells[n] = { label = label, value = value }
end
local separator = honor:CreateTexture(nil, "ARTWORK")
ForeverUI.SetAtlas(separator, LONG_SEPARATOR_ATLAS)
separator:SetPoint("TOP", honor, "BOTTOM", 0, HONOR_SEPARATOR_Y)
block.honorSeparator = separator
dial:SetPoint("TOP", separator, "BOTTOM", 0, DIAL_BELOW_SEPARATOR)
block.progress:SetPoint("TOP", dial, "BOTTOM", 0, PROGRESS_BELOW_DIAL)
local glow = dial:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(glow, GLOW_ATLAS, true)
glow:SetWidth(GLOW_W)
glow:SetHeight(GLOW_H)
glow:SetPoint("CENTER", dial, "CENTER", 0, 0)
local background = dial:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(background, string.format(ATLAS_BACKGROUND, string.lower(faction())), true)
background:SetWidth(BACKGROUND_W)
background:SetHeight(BACKGROUND_H)
background:SetPoint("CENTER", dial, "CENTER", 0, 0)
block.background = background
local ring = dial:CreateTexture(nil, "BORDER")
ForeverUI.SetAtlas(ring, RING_ATLAS, true)
ring:SetWidth(RING_W)
ring:SetHeight(RING_H)
ring:SetPoint("CENTER", dial, "CENTER", 0, RING_Y)
-- Gauge quarters, created here to draw above the ring and below the badge.
block.gauge = buildGauge(dial)
block.badge = dial:CreateTexture(nil, "ARTWORK")
block.badge:SetPoint("CENTER", dial, "CENTER", 0, 0)
-- Reward ring at the dial bottom, holding the rank number.
local reward = dial:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(reward, REWARD_RING_ATLAS)
reward:SetPoint("BOTTOM", dial, "BOTTOM", 0, REWARD_Y)
block.reward = reward
block.number = dial:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
block.number:SetPoint("CENTER", reward, "CENTER", 0, 0)
block.number:SetJustifyH("CENTER")
-- Arena teams and arena points at the bottom of the pane (PvPArena.lua).
if ForeverUI.PvPArena then
ForeverUI.PvPArena.build(block, host)
end
suppressClientScreen()
updateBlock()
return nil, { frame }
end
-- Right pane: battlegrounds (PvPBattlegrounds.lua).
local function buildDetail(host)
if detail then
return detail, {}
end
detail = CreateFrame("Frame", "ForeverUIPvPDetail", host)
detail:SetAllPoints(host)
if ForeverUI.PvPBattlegrounds then
ForeverUI.PvPBattlegrounds.build(detail)
end
return detail, {}
end
ForeverUI.PvPTab = { Build = build, BuildRight = buildDetail }
-- The client redraws its screen in PVPFrame_Update: run after it.
if hooksecurefunc and type(_G["PVPFrame_Update"]) == "function" then
hooksecurefunc("PVPFrame_Update", function()
suppressClientScreen()
updateBlock()
end)
end
-- Rank updates: KNOWN_TITLES_UPDATE announces a new rank title, PLAYER_PVP_KILLS_CHANGED moves
-- the gauge on each kill.
local listener = CreateFrame("Frame")
listener:RegisterEvent("KNOWN_TITLES_UPDATE")
listener:RegisterEvent("PLAYER_PVP_KILLS_CHANGED")
listener:RegisterEvent("PLAYER_PVP_RANK_CHANGED")
listener:RegisterEvent("HONOR_CURRENCY_UPDATE")
listener:SetScript("OnEvent", function()
if block then
updateBlock()
end
end)
-- Server channel (mod-pvp-titles-ext): addon whispers to the player.
-- RANKS:k1,...,k14 lifetime kills for each rank
-- DISHONOR:left,victims,required,window seconds left (0: not dishonored)
-- Requested again on each world entry: the login message can arrive before the UI, and
-- /reload loses it. Only whispers from the player himself are accepted.
local SERVER_PREFIX = "PVPTITLES"
-- Civilian mark in unit tooltips. CREATURE_FLAG_EXTRA_CIVILIAN is server data, missing from
-- the 3.3.5 creature query reply (CreatureTemplate::InitializeQueryData). The module answers:
-- CIV:<entry> whispered to self
-- CIV:<entry>:<0|1> the reply
-- The flag belongs to the creature template, so each entry is asked once per session.
-- Nothing is asked when the server sent no thresholds (module missing).
local CIVILIANS = {} -- entry -> true, false or "pending"
-- Creature entry from the GUID: 0xF130 (creature) or 0xF150 (vehicle), six hex digits of
-- entry, then the counter (ObjectGuid: counter | entry << 24 | high << 48). Players and pets
-- have none.
local function creatureEntry(unit)
local x = unit and string.match(UnitGUID(unit) or "", "^0x[Ff]1[35]0(%x%x%x%x%x%x)")
return x and tonumber(x, 16)
end
local function markCivilian(tooltipFrame)
tooltipFrame:AddLine(L.PVPTAB_TOOLTIP_CIVILIAN, HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b)
tooltipFrame:Show()
end
GameTooltip:HookScript("OnTooltipSetUnit", function(self)
local _, unit = self:GetUnit()
local entry = creatureEntry(unit)
if not entry then
return
end
if CIVILIANS[entry] == true then
markCivilian(self)
elseif CIVILIANS[entry] == nil and THRESHOLDS then
CIVILIANS[entry] = "pending"
SendAddonMessage(SERVER_PREFIX, "CIV:" .. entry, "WHISPER", UnitName("player"))
end
end)
-- The reply arrives after the tooltip is built: add the line if it still shows this creature.
local function onCivilianReply(entry, civilian)
CIVILIANS[entry] = civilian
if civilian and GameTooltip:IsShown() then
local _, unit = GameTooltip:GetUnit()
if creatureEntry(unit) == entry then
markCivilian(GameTooltip)
end
end
end
local channel = CreateFrame("Frame")
channel:RegisterEvent("PLAYER_ENTERING_WORLD")
channel:RegisterEvent("CHAT_MSG_ADDON")
channel:SetScript("OnEvent", function(_, event, prefix, message, distribution, sender)
if event == "PLAYER_ENTERING_WORLD" then
SendAddonMessage(SERVER_PREFIX, "REQ", "WHISPER", UnitName("player"))
return
end
if prefix ~= SERVER_PREFIX or distribution ~= "WHISPER" or sender ~= UnitName("player") then
return
end
local entry, civilian = string.match(message or "", "^CIV:(%d+):([01])$")
if entry then
onCivilianReply(tonumber(entry), civilian == "1")
return
end
local ranks = string.match(message or "", "^RANKS:(.+)$")
if ranks then
local parsed = {}
for n in string.gmatch(ranks, "%d+") do
parsed[#parsed + 1] = tonumber(n)
end
if #parsed == 14 then
THRESHOLDS = parsed
end
end
local rest = tonumber(string.match(message or "", "^DISHONOR:(%d+)") or "")
if rest then
DISHONOR.finish = (rest > 0) and (GetTime() + rest) or nil
updateDebuffs()
end
if block then
updateBlock()
end
if ForeverUI.Inspection and ForeverUI.Inspection.UpdatePvP then
ForeverUI.Inspection.UpdatePvP()
end
end)
-- Dishonored countdown, updated every second while the sheet is open; the rank returns at the
-- end.
local elapsed = 0
channel:SetScript("OnUpdate", function(_, e)
if not DISHONOR.finish then
return
end
elapsed = elapsed + e
if elapsed < 1 then
return
end
elapsed = 0
local rest = DISHONOR.finish - GetTime()
if rest <= 0 then
DISHONOR.finish = nil
updateDebuffs()
if block then
updateBlock()
end
elseif block and block:IsVisible() then
block.progress:SetText(string.format(L.PVPTAB_DISHONORED_LEFT, SecondsToTime(rest)))
end
end)