ca2b78e217
/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>
1242 lines
44 KiB
Lua
1242 lines
44 KiB
Lua
-- Camelot's quest and achievement tracker on the WotLK WatchFrame (docs/SUIVI_DES_QUETES.md).
|
|
-- WatchFrame, its events and CVars, VISIBLE_WATCHES, WatchFrameItem<n> and the handler API stay;
|
|
-- only the three WotLK display handlers are replaced by camelot's two modules.
|
|
-- Missing from 3.3.5, so absent here: Focus tracking, progress bars, automatic quest popups.
|
|
|
|
ForeverUI = ForeverUI or {}
|
|
|
|
local T = {}
|
|
ForeverUI.ObjectiveTracker = T
|
|
local L = ForeverUI.L
|
|
|
|
local G = {
|
|
width = 260, headerH = 32, headerTextX = 7, headerTextL = 208,
|
|
buttonW = 18, buttonH = 19, buttonX = -1, filterX = -2,
|
|
modulesTop = 38, rowsBottom = 12,
|
|
moduleHeaderH = 26, moduleCountedHeader = 25, moduleTextL = 200, moduleButtonSide = 16, moduleButtonX = 1,
|
|
blockX = 20, firstBlockY = -10, blockY = -10, rowGap = 4,
|
|
itemSide = 26, itemFrame = 42, rightGap = 2,
|
|
checkMark = 16, checkMarkX = -10, checkMarkY = 2,
|
|
poiX = -17, poiY = -5,
|
|
rowGlowL = 180, headerGlowL = 240,
|
|
timerW = 192, timerH = 20, barW = 128, barH = 10, barX = -4,
|
|
maxCriteria = 5,
|
|
-- default place: camelot EditModePresetLayouts, ObjectiveTracker system,
|
|
-- TOPRIGHT of UIParent at (-110, -275)
|
|
defaultX = -110, defaultY = -275, minHeight = 140,
|
|
}
|
|
|
|
-- OBJECTIVE_TRACKER_COLOR, values from camelot's GlobalColor.db2
|
|
local COLOR = {
|
|
normal = { 0.8, 0.8, 0.8 }, normalHover = { 1, 1, 1 },
|
|
failed = { 0.8, 0.098, 0.098 }, failedHover = { 1, 0.125, 0.125 },
|
|
header = { 0.749, 0.612, 0 }, headerHover = { 1, 0.824, 0 },
|
|
finished = { 0.6, 0.6, 0.6 },
|
|
title = { 1, 0.824, 0 }, -- NORMAL_FONT_COLOR
|
|
bar = { 0.26, 0.42, 1 }, barBackground = { 0.04, 0.07, 0.18 },
|
|
}
|
|
COLOR.normal.inverse = COLOR.normalHover
|
|
COLOR.normalHover.inverse = COLOR.normal
|
|
COLOR.failed.inverse = COLOR.failedHover
|
|
COLOR.failedHover.inverse = COLOR.failed
|
|
|
|
-- 3.3.5 strings when it has them, camelot's otherwise (Textes_<locale>.lua)
|
|
local TEXT = {
|
|
shareInChat = L.OBJECTIVETRACKER_SHARE_IN_CHAT, -- SHARE_IN_CHAT
|
|
all = L.OBJECTIVETRACKER_ALL_OBJECTIVES, -- TRACKER_ALL_OBJECTIVES
|
|
quests = QUESTS_LABEL, -- TRACKER_HEADER_QUESTS (3.3.5: QUESTS_LABEL)
|
|
achievements = ACHIEVEMENTS, -- TRACKER_HEADER_ACHIEVEMENTS (3.3.5: ACHIEVEMENTS)
|
|
ready = L.OBJECTIVETRACKER_READY, -- QUEST_WATCH_QUEST_READY
|
|
viewPage = OBJECTIVES_VIEW_IN_QUESTLOG, -- OBJECTIVES_VIEW_IN_QUESTLOG (in 3.3.5)
|
|
viewMap = L.OBJECTIVETRACKER_OPEN_MAP, -- OBJECTIVES_SHOW_QUEST_MAP
|
|
untrack = L.OBJECTIVETRACKER_UNTRACK, -- OBJECTIVES_STOP_TRACKING
|
|
abandonQuest = ABANDON_QUEST_ABBREV, -- ABANDON_QUEST_ABBREV (in 3.3.5)
|
|
viewAchievement = OBJECTIVES_VIEW_ACHIEVEMENT, -- OBJECTIVES_VIEW_ACHIEVEMENT (in 3.3.5)
|
|
minutes = "%.2d:%.2d", -- MINUTES_SECONDS
|
|
hours = "%.2d:%.2d:%.2d", -- HOURS_MINUTES_SECONDS
|
|
}
|
|
|
|
local FONT = "Fonts\\FRIZQT__.TTF"
|
|
|
|
local function color(fs, c)
|
|
fs:SetTextColor(c[1], c[2], c[3])
|
|
fs.color = c
|
|
end
|
|
|
|
-- ObjectiveTrackerLineFont (12) and ObjectiveTrackerHeaderFont (14), black shadow (1, -1)
|
|
local function font(fs, size)
|
|
fs:SetFont(FONT, size)
|
|
fs:SetShadowOffset(1, -1)
|
|
fs:SetShadowColor(0, 0, 0, 1)
|
|
fs:SetJustifyH("LEFT")
|
|
fs:SetJustifyV("TOP")
|
|
end
|
|
|
|
local function settings()
|
|
ForeverUIDB = ForeverUIDB or {}
|
|
ForeverUIDB.tracking = ForeverUIDB.tracking or {}
|
|
ForeverUIDB.tracking.collapsedModules = ForeverUIDB.tracking.collapsedModules or {}
|
|
return ForeverUIDB.tracking
|
|
end
|
|
|
|
-- camelot SecondsToClock
|
|
local function clock(seconds)
|
|
seconds = math.max(0, math.floor(seconds))
|
|
local h = math.floor(seconds / 3600)
|
|
local m = math.floor((seconds % 3600) / 60)
|
|
local s = seconds % 60
|
|
if h > 0 then
|
|
return format(TEXT.hours, h, m, s)
|
|
end
|
|
return format(TEXT.minutes, m, s)
|
|
end
|
|
|
|
-- ---------- Animations
|
|
-- One OnUpdate plays every running step: a delay, a duration, and a function that receives
|
|
-- the progress from 0 to 1. 3.3.5 animation groups lack fromAlpha / toAlpha and
|
|
-- setToFinalAlpha, so camelot's animations are replayed by hand with its timings.
|
|
-- A finish callback may start new steps (a chained step, a WatchFrame_Update): finish
|
|
-- callbacks run after the loop, since Lua leaves a traversal undefined when a key is added
|
|
-- during it, and the frame stops only once no step is left.
|
|
local A = { steps = {} }
|
|
A.frame = CreateFrame("Frame")
|
|
A.frame:Hide()
|
|
A.frame:SetScript("OnUpdate", function(self, elapsed)
|
|
local finished
|
|
for key, e in pairs(A.steps) do
|
|
e.t = e.t + elapsed
|
|
if e.t >= e.delay then
|
|
local p = (e.duration > 0) and math.min(1, (e.t - e.delay) / e.duration) or 1
|
|
e.fn(p)
|
|
if p >= 1 then
|
|
A.steps[key] = nil
|
|
if e.finish then
|
|
finished = finished or {}
|
|
table.insert(finished, e.finish)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if finished then
|
|
for _, finish in ipairs(finished) do finish() end
|
|
end
|
|
if not next(A.steps) then self:Hide() end
|
|
end)
|
|
|
|
-- key: { object, name }, a step with the same key replaces the running one;
|
|
-- fn(progress) runs each frame; finish: called once done
|
|
local function play(key, delay, duration, fn, finish)
|
|
key = tostring(key[1]) .. key[2]
|
|
A.steps[key] = { t = 0, delay = delay, duration = duration, fn = fn, finish = finish }
|
|
A.frame:Show()
|
|
end
|
|
T.play = play
|
|
T.animations = A
|
|
|
|
-- smoothing="OUT"
|
|
local function easeOut(p) return 1 - (1 - p) * (1 - p) end
|
|
|
|
-- ---------- Buttons
|
|
-- atlasButton: l, h: size; normal, pressed, hover: atlas names
|
|
local function atlasButton(parent, l, h, normal, pressed, hover)
|
|
local b = CreateFrame("Button", nil, parent)
|
|
b:SetWidth(l)
|
|
b:SetHeight(h)
|
|
local e = ForeverUI.AtlasEntry(normal)
|
|
b:SetNormalTexture(e and e[1] or "")
|
|
ForeverUI.SetAtlas(b:GetNormalTexture(), normal, true)
|
|
b:GetNormalTexture():SetAllPoints(b)
|
|
b:SetPushedTexture(e and e[1] or "")
|
|
ForeverUI.SetAtlas(b:GetPushedTexture(), pressed, true)
|
|
b:GetPushedTexture():SetAllPoints(b)
|
|
b:SetHighlightTexture(e and e[1] or "")
|
|
local s = b:GetHighlightTexture()
|
|
ForeverUI.SetAtlas(s, hover, true)
|
|
s:SetAllPoints(b)
|
|
s:SetBlendMode("ADD")
|
|
return b
|
|
end
|
|
|
|
local function buttonStates(b, normal, pressed)
|
|
ForeverUI.SetAtlas(b:GetNormalTexture(), normal, true)
|
|
ForeverUI.SetAtlas(b:GetPushedTexture(), pressed, true)
|
|
end
|
|
|
|
-- opens a dropdown through the world map's menu helper
|
|
local function menu(anchor, list)
|
|
if ForeverUI.WorldMap and ForeverUI.WorldMap.openMenu then
|
|
ForeverUI.WorldMap.openMenu(anchor, list)
|
|
end
|
|
end
|
|
|
|
-- ---------- Main header
|
|
-- ObjectiveTrackerContainerHeaderTemplate, on WatchFrame
|
|
local function createHeader()
|
|
local h = CreateFrame("Frame", "ForeverUIObjectiveTrackerHeader", WatchFrame)
|
|
h:SetWidth(G.width)
|
|
h:SetHeight(G.headerH)
|
|
h:SetPoint("TOPLEFT", WatchFrame, "TOPLEFT", 0, 0)
|
|
local background = h:CreateTexture(nil, "BACKGROUND")
|
|
ForeverUI.SetAtlas(background, "ui-questtracker-primary-objective-header")
|
|
background:SetPoint("CENTER", h, "CENTER", 0, 0)
|
|
h.background = background
|
|
local text = h:CreateFontString(nil, "ARTWORK")
|
|
font(text, 14)
|
|
color(text, COLOR.title)
|
|
text:SetWidth(G.headerTextL)
|
|
text:SetJustifyV("MIDDLE")
|
|
text:SetPoint("LEFT", h, "LEFT", G.headerTextX, 0)
|
|
text:SetText(TEXT.all)
|
|
h.text = text
|
|
|
|
local shrink = atlasButton(h, G.buttonW, G.buttonH, "ui-questtrackerbutton-collapse-all",
|
|
"ui-questtrackerbutton-collapse-all-pressed", "ui-questtrackerbutton-red-highlight")
|
|
shrink:SetPoint("RIGHT", h, "RIGHT", G.buttonX, 0)
|
|
-- WatchFrame_CollapseExpandButton_OnClick, with camelot's sound
|
|
shrink:SetScript("OnClick", function()
|
|
PlaySound("igMainMenuOptionCheckBoxOn")
|
|
if WatchFrame.collapsed then
|
|
WatchFrame.userCollapsed = nil
|
|
WatchFrame_Expand(WatchFrame)
|
|
else
|
|
WatchFrame.userCollapsed = true
|
|
WatchFrame_Collapse(WatchFrame)
|
|
end
|
|
end)
|
|
h.shrink = shrink
|
|
|
|
-- filter button (camelot has it but hides it): WotLK sorting and filters
|
|
local filter = atlasButton(h, G.buttonW, G.buttonH, "ui-questtrackerbutton-filter",
|
|
"ui-questtrackerbutton-filter-pressed", "ui-questtrackerbutton-red-highlight")
|
|
filter:SetPoint("RIGHT", shrink, "LEFT", G.filterX, 0)
|
|
filter:SetScript("OnClick", function(self)
|
|
PlaySound("igMainMenuOptionCheckBoxOn")
|
|
T.filterMenu(self)
|
|
end)
|
|
h.filter = filter
|
|
h:Hide()
|
|
return h
|
|
end
|
|
|
|
-- WatchFrameHeaderDropDown_Initialize, entry for entry
|
|
function T.filterMenu(anchor)
|
|
local sort = WATCHFRAME_SORT_TYPE
|
|
local filter = WATCHFRAME_FILTER_TYPE
|
|
local function sortBy(value)
|
|
return function() WatchFrame_SetSorting(nil, value) end
|
|
end
|
|
local function filterBy(value)
|
|
return function() WatchFrame_SetFilter(nil, value) end
|
|
end
|
|
local function active(bit_)
|
|
return bit.band(filter, bit_) == bit_
|
|
end
|
|
menu(anchor, {
|
|
{ text = TRACKER_SORT_LABEL, isTitle = true },
|
|
{ text = TRACKER_SORT_PROXIMITY, checked = sort == WATCHFRAME_SORT_PROXIMITY, func = sortBy(WATCHFRAME_SORT_PROXIMITY) },
|
|
{ text = TRACKER_SORT_DIFFICULTY_HIGH, checked = sort == WATCHFRAME_SORT_DIFFICULTY_HIGH, func = sortBy(WATCHFRAME_SORT_DIFFICULTY_HIGH) },
|
|
{ text = TRACKER_SORT_DIFFICULTY_LOW, checked = sort == WATCHFRAME_SORT_DIFFICULTY_LOW, func = sortBy(WATCHFRAME_SORT_DIFFICULTY_LOW) },
|
|
{ text = TRACKER_SORT_MANUAL, checked = sort == WATCHFRAME_SORT_MANUAL, func = sortBy(WATCHFRAME_SORT_MANUAL) },
|
|
{ text = TRACKER_FILTER_LABEL, isTitle = true },
|
|
{ text = TRACKER_FILTER_ACHIEVEMENTS, checked = active(WATCHFRAME_FILTER_ACHIEVEMENTS), keepShownOnClick = 1, func = filterBy(WATCHFRAME_FILTER_ACHIEVEMENTS) },
|
|
{ text = TRACKER_FILTER_COMPLETED_QUESTS, checked = active(WATCHFRAME_FILTER_COMPLETED_QUESTS), keepShownOnClick = 1, func = filterBy(WATCHFRAME_FILTER_COMPLETED_QUESTS) },
|
|
{ text = TRACKER_FILTER_REMOTE_ZONES, checked = active(WATCHFRAME_FILTER_REMOTE_ZONES), keepShownOnClick = 1, func = filterBy(WATCHFRAME_FILTER_REMOTE_ZONES) },
|
|
})
|
|
end
|
|
|
|
-- ---------- Modules
|
|
-- ObjectiveTrackerModuleTemplate: a header, then the blocks.
|
|
-- title: header text; key: saved collapse state key
|
|
local function createModule(name, title, key)
|
|
local m = CreateFrame("Frame", name, WatchFrameLines)
|
|
m:SetWidth(G.width)
|
|
m:SetHeight(10)
|
|
m.key = key
|
|
local h = CreateFrame("Frame", nil, m)
|
|
h:SetWidth(G.width)
|
|
h:SetHeight(G.moduleHeaderH)
|
|
h:SetPoint("TOPLEFT", m, "TOPLEFT", 0, 0)
|
|
local background = h:CreateTexture(nil, "BACKGROUND")
|
|
ForeverUI.SetAtlas(background, "ui-questtracker-secondary-objective-header")
|
|
background:SetPoint("CENTER", h, "CENTER", 0, 0)
|
|
local text = h:CreateFontString(nil, "ARTWORK")
|
|
font(text, 14)
|
|
color(text, COLOR.title)
|
|
text:SetWidth(G.moduleTextL)
|
|
text:SetJustifyV("MIDDLE")
|
|
text:SetPoint("LEFT", h, "LEFT", G.headerTextX, 0)
|
|
text:SetText(title)
|
|
local shine = h:CreateTexture(nil, "ARTWORK")
|
|
ForeverUI.SetAtlas(shine, "ui-questtracker-objfx-shine")
|
|
shine:SetWidth(shine:GetWidth() * 0.95)
|
|
shine:SetHeight(shine:GetHeight() * 0.95)
|
|
shine:SetPoint("CENTER", h, "CENTER", -150, 1)
|
|
shine:SetAlpha(0)
|
|
local glow = h:CreateTexture(nil, "OVERLAY")
|
|
ForeverUI.SetAtlas(glow, "ui-questtracker-objfx-barglow")
|
|
glow:SetPoint("CENTER", h, "CENTER", -120, 1)
|
|
glow:SetAlpha(0)
|
|
local button = atlasButton(h, G.moduleButtonSide, G.moduleButtonSide, "ui-questtrackerbutton-secondary-collapse",
|
|
"ui-questtrackerbutton-secondary-collapse-pressed", "ui-questtrackerbutton-yellow-highlight")
|
|
button:SetPoint("RIGHT", h, "RIGHT", G.moduleButtonX, 0)
|
|
button:SetScript("OnClick", function()
|
|
PlaySound("igMainMenuOptionCheckBoxOn")
|
|
local r = settings().collapsedModules
|
|
r[key] = not r[key] or nil
|
|
WatchFrame_Update()
|
|
end)
|
|
h.background, h.text, h.shine, h.glow, h.button = background, text, shine, glow, button
|
|
m.header = h
|
|
m.blocks, m.freeBlocks = {}, {}
|
|
m:Hide()
|
|
return m
|
|
end
|
|
|
|
-- ObjectiveTrackerModuleHeaderMixin:PlayAddAnimation
|
|
local function animateModuleHeader(m)
|
|
local h = m.header
|
|
local key = m
|
|
h.background:SetAlpha(0)
|
|
h.button:SetAlpha(0)
|
|
play({ key, "background" }, 0, 0.5, function(p) h.background:SetAlpha(p) end)
|
|
play({ key, "button" }, 0, 1, function(p) h.button:SetAlpha(p) end)
|
|
play({ key, "glow" }, 0, 0.2, function(p) h.glow:SetAlpha(p) end, function()
|
|
play({ key, "glow" }, 0, 0.6, function(p) h.glow:SetAlpha(1 - p) end)
|
|
end)
|
|
play({ key, "shine" }, 0.2, 1.2, function(p)
|
|
local q = math.min(1, p * 1.2 / 0.7)
|
|
h.shine:ClearAllPoints()
|
|
h.shine:SetPoint("CENTER", h, "CENTER", -150 + 200 * q, 1)
|
|
h.shine:SetAlpha(1 - p)
|
|
end, function() h.shine:SetAlpha(0) end)
|
|
end
|
|
|
|
-- ---------- Blocks
|
|
local function createRow(block)
|
|
local l = CreateFrame("Frame", nil, block)
|
|
local dash = l:CreateFontString(nil, "ARTWORK")
|
|
font(dash, 12)
|
|
dash:SetPoint("TOPLEFT", l, "TOPLEFT", 0, 1)
|
|
dash:SetText(QUEST_DASH)
|
|
local text = l:CreateFontString(nil, "ARTWORK")
|
|
font(text, 12)
|
|
text:SetPoint("TOP", l, "TOP", 0, 0)
|
|
text:SetPoint("LEFT", dash, "RIGHT", 0, 0)
|
|
local checkMark = l:CreateTexture(nil, "ARTWORK")
|
|
ForeverUI.SetAtlas(checkMark, "ui-questtracker-tracker-check", true)
|
|
checkMark:SetWidth(G.checkMark)
|
|
checkMark:SetHeight(G.checkMark)
|
|
checkMark:SetPoint("TOPLEFT", l, "TOPLEFT", G.checkMarkX, G.checkMarkY)
|
|
checkMark:Hide()
|
|
local flare = l:CreateTexture(nil, "OVERLAY")
|
|
ForeverUI.SetAtlas(flare, "ui-questtracker-tracker-check-glow", true)
|
|
flare:SetWidth(G.checkMark)
|
|
flare:SetHeight(G.checkMark)
|
|
flare:SetPoint("CENTER", checkMark, "CENTER", 0, 0)
|
|
flare:SetAlpha(0)
|
|
local glow = l:CreateTexture(nil, "OVERLAY")
|
|
ForeverUI.SetAtlas(glow, "ui-questtracker-objfx-barglow", true)
|
|
glow:SetWidth(G.rowGlowL)
|
|
glow:SetPoint("LEFT", text, "LEFT", -2, 0)
|
|
glow:SetPoint("TOP", l, "TOP", 0, 0)
|
|
glow:SetPoint("BOTTOM", l, "BOTTOM", 0, -4)
|
|
glow:SetAlpha(0)
|
|
l.dash, l.text, l.checkMark, l.flare, l.glow = dash, text, checkMark, flare, glow
|
|
return l
|
|
end
|
|
|
|
-- ObjectiveTrackerTimerBarTemplate
|
|
local function createTimer(block)
|
|
local m = CreateFrame("Frame", nil, block)
|
|
m:SetWidth(G.timerW)
|
|
m:SetHeight(G.timerH)
|
|
-- camelot GameFontHighlightMedium: FRIZQT 14, white, shadow
|
|
local text = m:CreateFontString(nil, "ARTWORK")
|
|
text:SetFont(FONT, 14)
|
|
text:SetShadowOffset(1, -1)
|
|
text:SetShadowColor(0, 0, 0, 1)
|
|
text:SetJustifyH("LEFT")
|
|
text:SetPoint("LEFT", m, "LEFT", 0, 0)
|
|
local bar = CreateFrame("StatusBar", nil, m)
|
|
bar:SetWidth(G.barW)
|
|
bar:SetHeight(G.barH)
|
|
bar:SetPoint("RIGHT", m, "RIGHT", G.barX, 0)
|
|
bar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
|
|
bar:SetStatusBarColor(COLOR.bar[1], COLOR.bar[2], COLOR.bar[3])
|
|
local background = bar:CreateTexture(nil, "BACKGROUND")
|
|
background:SetTexture(COLOR.barBackground[1], COLOR.barBackground[2], COLOR.barBackground[3])
|
|
background:SetAllPoints(bar)
|
|
local BAR_BORDER = "Interface\\PaperDollInfoFrame\\UI-Character-Skills-BarBorder"
|
|
local g = bar:CreateTexture(nil, "ARTWORK")
|
|
g:SetTexture(BAR_BORDER)
|
|
g:SetTexCoord(0.007843, 0.043137, 0.193548, 0.774193)
|
|
g:SetWidth(9) g:SetHeight(14)
|
|
g:SetPoint("LEFT", bar, "LEFT", -3, 0)
|
|
local d = bar:CreateTexture(nil, "ARTWORK")
|
|
d:SetTexture(BAR_BORDER)
|
|
d:SetTexCoord(0.043137, 0.007843, 0.193548, 0.774193)
|
|
d:SetWidth(9) d:SetHeight(14)
|
|
d:SetPoint("RIGHT", bar, "RIGHT", 3, 0)
|
|
local mid = bar:CreateTexture(nil, "ARTWORK")
|
|
mid:SetTexture(BAR_BORDER)
|
|
mid:SetTexCoord(0.113726, 0.1490196, 0.193548, 0.774193)
|
|
mid:SetPoint("TOPLEFT", g, "TOPRIGHT", 0, 0)
|
|
mid:SetPoint("BOTTOMRIGHT", d, "BOTTOMLEFT", 0, 0)
|
|
m.text, m.bar = text, bar
|
|
-- ObjectiveTrackerTimerBarMixin:OnUpdate and GetTextColor
|
|
m:SetScript("OnUpdate", function(self)
|
|
if not self.duration then return end
|
|
local rest = self.duration - (GetTime() - self.start)
|
|
self.bar:SetValue(math.max(0, rest))
|
|
if rest < -1 then
|
|
self.duration = nil
|
|
WatchFrame_Update()
|
|
return
|
|
end
|
|
rest = math.max(0, rest)
|
|
self.text:SetText(clock(rest))
|
|
local part = rest / self.duration
|
|
if part > 0.66 then
|
|
self.text:SetTextColor(1, 1, 1)
|
|
elseif part > 0.33 then
|
|
self.text:SetTextColor(1, 1, (part - 0.33) / 0.33)
|
|
else
|
|
self.text:SetTextColor(1, part / 0.33, 0)
|
|
end
|
|
end)
|
|
return m
|
|
end
|
|
|
|
local function createBlock(m)
|
|
local b = CreateFrame("Frame", nil, m)
|
|
local title = b:CreateFontString(nil, "ARTWORK")
|
|
font(title, 12)
|
|
title:SetPoint("TOPLEFT", b, "TOPLEFT", 0, 0)
|
|
b.title = title
|
|
local glow = b:CreateTexture(nil, "OVERLAY")
|
|
ForeverUI.SetAtlas(glow, "ui-questtracker-objfx-barglow", true)
|
|
glow:SetWidth(G.headerGlowL)
|
|
glow:SetPoint("TOPLEFT", title, "TOPLEFT", 0, 3)
|
|
glow:SetPoint("BOTTOMLEFT", title, "BOTTOMLEFT", 0, -4)
|
|
glow:SetAlpha(0)
|
|
b.glow = glow
|
|
-- HeaderButton: over the title, left and right click
|
|
local button = CreateFrame("Button", nil, b)
|
|
button:SetPoint("TOPLEFT", title, "TOPLEFT", 0, 0)
|
|
button:SetPoint("BOTTOMRIGHT", title, "BOTTOMRIGHT", 0, 0)
|
|
button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
|
button:SetScript("OnClick", function(_, mouse) m.onClick(b, mouse) end)
|
|
button:SetScript("OnEnter", function() T.toggleHighlight(b, true) end)
|
|
button:SetScript("OnLeave", function() T.toggleHighlight(b, false) end)
|
|
b.button = button
|
|
b.rows, b.freeRows = {}, {}
|
|
return b
|
|
end
|
|
|
|
-- ObjectiveTrackerBlockMixin:UpdateHighlight; yes: hovered
|
|
function T.toggleHighlight(b, yes)
|
|
b.hover = yes
|
|
color(b.title, yes and COLOR.headerHover or COLOR.header)
|
|
local dash = yes and COLOR.normalHover or COLOR.normal
|
|
for _, l in ipairs(b.shownRows or {}) do
|
|
local c = l.text.color
|
|
if c and c.inverse and ((yes and (c == COLOR.normal or c == COLOR.failed))
|
|
or (not yes and (c == COLOR.normalHover or c == COLOR.failedHover))) then
|
|
color(l.text, c.inverse)
|
|
end
|
|
l.dash:SetTextColor(dash[1], dash[2], dash[3])
|
|
end
|
|
end
|
|
|
|
-- A block starts from scratch on every pass (Reset), and so do its rows.
|
|
local function acquireBlock(m, id)
|
|
local b = m.blocks[id]
|
|
if not b then
|
|
b = table.remove(m.freeBlocks) or createBlock(m)
|
|
m.blocks[id] = b
|
|
end
|
|
b.id, b.inUse = id, true
|
|
b:SetWidth(G.width - G.blockX)
|
|
b.contentHeight = 0
|
|
b.last = nil
|
|
b.right = 0
|
|
b.shownRows = {}
|
|
for _, l in pairs(b.rows) do l.used = nil end
|
|
if b.timer then b.timer:Hide() b.timer.duration = nil end
|
|
b:ClearAllPoints()
|
|
b:SetAlpha(1)
|
|
b.title:SetAlpha(1)
|
|
return b
|
|
end
|
|
|
|
-- Widths are set by hand: the block is not placed yet when its texts are measured, and
|
|
-- 3.3.5 only measures a text that has a width (camelot relies on anchors).
|
|
-- camelot 70450 block title (QuestObjectiveTrackerMixin:UpdateSingle with QuestUtilsOverrides):
|
|
-- "[" level ("+" if elite) "] " title, then the quest type ("(Elite)", "(Dungeon)"...). The
|
|
-- difficulty color (showQuestDifficultyColor) is a modern CVar 3.3.5 lacks: not applied.
|
|
local function trackerTitle(title, level, questTag)
|
|
if not title then
|
|
return title
|
|
end
|
|
local tagText = ForeverUI.QuestTagText and ForeverUI.QuestTagText(questTag)
|
|
if tagText then
|
|
title = title .. " " .. tagText
|
|
end
|
|
if level and level > 0 then
|
|
title = "[" .. level .. ((questTag == ELITE) and "+" or "") .. "] " .. title
|
|
end
|
|
return title
|
|
end
|
|
|
|
local function setBlockTitle(b, text)
|
|
b.title:SetWidth(G.width - G.blockX + b.right)
|
|
b.title:SetHeight(0)
|
|
b.title:SetText(text)
|
|
color(b.title, b.hover and COLOR.headerHover or COLOR.header)
|
|
b.contentHeight = b.title:GetHeight()
|
|
end
|
|
|
|
-- ObjectiveTrackerBlockMixin:AddObjective. key: row id in the block; dash: show the dash;
|
|
-- c: text color (default normal)
|
|
local function row(b, key, text, dash, c)
|
|
local l = b.rows[key]
|
|
if not l then
|
|
l = table.remove(b.freeRows) or createRow(b)
|
|
b.rows[key] = l
|
|
end
|
|
l.used = true
|
|
l:ClearAllPoints()
|
|
l:SetPoint("TOPLEFT", b.last or b.title, "BOTTOMLEFT", 0, -G.rowGap)
|
|
local width = G.width - G.blockX + b.right
|
|
l:SetWidth(width)
|
|
l.dash:SetText(QUEST_DASH)
|
|
if dash then l.dash:Show() else l.dash:Hide() end
|
|
l.text:SetWidth(width - l.dash:GetStringWidth())
|
|
l.text:SetHeight(0)
|
|
l.text:SetText(text)
|
|
c = c or COLOR.normal
|
|
if b.hover and c.inverse then c = c.inverse end
|
|
color(l.text, c)
|
|
local tc = b.hover and COLOR.normalHover or COLOR.normal
|
|
l.dash:SetTextColor(tc[1], tc[2], tc[3])
|
|
local h = l.text:GetHeight()
|
|
l:SetHeight(h)
|
|
l:SetAlpha(1)
|
|
l:Show()
|
|
b.contentHeight = b.contentHeight + h + G.rowGap
|
|
b.last = l
|
|
table.insert(b.shownRows, l)
|
|
return l
|
|
end
|
|
|
|
-- ObjectiveTrackerBlockMixin:AddTimerBar; start: GetTime() when the timer began
|
|
local function timer(b, duration, start)
|
|
if not b.timer then b.timer = createTimer(b) end
|
|
local m = b.timer
|
|
m:ClearAllPoints()
|
|
m:SetPoint("TOPLEFT", b.last or b.title, "BOTTOMLEFT", 0, -G.rowGap)
|
|
m.bar:SetMinMaxValues(0, duration)
|
|
m.duration, m.start = duration, start
|
|
m:Show()
|
|
b.contentHeight = b.contentHeight + G.timerH + G.rowGap
|
|
b.last = m
|
|
end
|
|
|
|
-- rows and blocks not reused in this pass are released
|
|
local function releaseRows(b)
|
|
for key, l in pairs(b.rows) do
|
|
if not l.used then
|
|
l:Hide()
|
|
l.checkMark:Hide()
|
|
l.glow:SetAlpha(0)
|
|
l.flare:SetAlpha(0)
|
|
b.rows[key] = nil
|
|
table.insert(b.freeRows, l)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function releaseBlocks(m)
|
|
for id, b in pairs(m.blocks) do
|
|
if not b.inUse then
|
|
b:Hide()
|
|
b.hover = nil
|
|
m.blocks[id] = nil
|
|
table.insert(m.freeBlocks, b)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- check state of a row: Completed = check mark shown, no animation;
|
|
-- Completing (animate) = check mark + flare + glow (CheckAnim and GlowAnim)
|
|
local function checkOff(l, animate)
|
|
l.checkMark:Show()
|
|
if not animate then
|
|
l.checkMark:SetAlpha(1)
|
|
return
|
|
end
|
|
play({ l, "checkMark" }, 0, 0.3, function(p)
|
|
local s = (p < 0.5) and (1 + 0.2 * easeOut(p * 2)) or (1.2 - 0.2 * easeOut((p - 0.5) * 2))
|
|
l.checkMark:SetWidth(G.checkMark * s)
|
|
l.checkMark:SetHeight(G.checkMark * s)
|
|
l.checkMark:SetAlpha(math.min(1, p / 0.53))
|
|
l.flare:SetWidth(G.checkMark * s)
|
|
l.flare:SetHeight(G.checkMark * s)
|
|
l.flare:SetAlpha(p < 0.5 and p * 2 or (1 - p) * 2)
|
|
end, function()
|
|
l.checkMark:SetWidth(G.checkMark) l.checkMark:SetHeight(G.checkMark) l.checkMark:SetAlpha(1)
|
|
l.flare:SetAlpha(0)
|
|
end)
|
|
T.sweep(l.glow, G.rowGlowL, 0.1, 0.66, 0.33, 0.58)
|
|
end
|
|
|
|
-- a glow that stretches from the left (Scale x 0 -> 1, origin LEFT), then fades out
|
|
-- (Alpha 1 -> 0)
|
|
function T.sweep(t, width, delay, duration, alphaDelay, alphaDuration)
|
|
play({ t, "width" }, delay, duration, function(p)
|
|
t:SetWidth(math.max(1, width * easeOut(p)))
|
|
end)
|
|
play({ t, "alpha" }, alphaDelay, alphaDuration, function(p) t:SetAlpha(1 - p) end,
|
|
function() t:SetAlpha(0) t:SetWidth(width) end)
|
|
end
|
|
|
|
-- ObjectiveTrackerAnimBlockMixin:PlayAddAnimation: title glow, then the title and rows fade in
|
|
local function animateAdd(b)
|
|
b.glow:SetWidth(1)
|
|
b.glow:SetAlpha(1)
|
|
play({ b.glow, "width" }, 0.15, 0.31, function(p) b.glow:SetWidth(math.max(1, G.headerGlowL * p)) end)
|
|
play({ b.glow, "alpha" }, 0.33, 0.41, function(p) b.glow:SetAlpha(1 - p) end,
|
|
function() b.glow:SetAlpha(0) b.glow:SetWidth(G.headerGlowL) end)
|
|
b.title:SetAlpha(0)
|
|
play({ b.title, "alpha" }, 0, 0.03, function(p) b.title:SetAlpha(p) end)
|
|
for _, l in ipairs(b.shownRows) do
|
|
l:SetAlpha(0)
|
|
play({ l, "entry" }, 0, 0.5, function(p) l:SetAlpha(p) end)
|
|
end
|
|
end
|
|
|
|
-- ---------- Layout
|
|
-- ObjectiveTrackerModuleMixin: BeginLayout, AddBlock / CanFitBlock, EndLayout.
|
|
-- position: height available; finalize returns the module height, 0 when hidden.
|
|
local function begin(m, position)
|
|
m.position = position
|
|
m.contentHeight = G.moduleCountedHeader
|
|
m.last = nil
|
|
m.content = false
|
|
m.skipped = false
|
|
m.tried = false
|
|
for _, b in pairs(m.blocks) do b.inUse = nil end
|
|
end
|
|
|
|
-- returns true when the block fits
|
|
local function place(m, b)
|
|
m.tried = true
|
|
releaseRows(b)
|
|
b:SetHeight(math.max(1, b.contentHeight))
|
|
local gap = m.last and G.blockY or G.firstBlockY
|
|
if m.contentHeight + b.contentHeight - gap > m.position then
|
|
m.skipped = true
|
|
b.inUse = nil
|
|
return false
|
|
end
|
|
m.content = true
|
|
if settings().collapsedModules[m.key] then
|
|
b.inUse = nil
|
|
return true
|
|
end
|
|
b:ClearAllPoints()
|
|
if m.last then
|
|
b:SetPoint("TOP", m.last, "BOTTOM", 0, gap)
|
|
else
|
|
b:SetPoint("TOP", m.header, "BOTTOM", 0, gap)
|
|
end
|
|
b:SetPoint("LEFT", m, "LEFT", G.blockX, 0)
|
|
b:SetPoint("RIGHT", m, "RIGHT", 0, 0)
|
|
b:Show()
|
|
m.contentHeight = m.contentHeight + b.contentHeight - gap
|
|
m.last = b
|
|
return true
|
|
end
|
|
|
|
local function finalize(m, lineFrame, offset)
|
|
releaseBlocks(m)
|
|
local collapsed = settings().collapsedModules[m.key]
|
|
buttonStates(m.header.button,
|
|
collapsed and "ui-questtrackerbutton-secondary-expand" or "ui-questtrackerbutton-secondary-collapse",
|
|
collapsed and "ui-questtrackerbutton-secondary-expand-pressed" or "ui-questtrackerbutton-secondary-collapse-pressed")
|
|
if m.content then
|
|
m:ClearAllPoints()
|
|
m:SetPoint("TOPLEFT", lineFrame, "TOPLEFT", 0, offset)
|
|
m:SetHeight(m.contentHeight)
|
|
local wasShown = m:IsShown() and m.displayed
|
|
m:Show()
|
|
m.displayed = true
|
|
if not wasShown then animateModuleHeader(m) end
|
|
return m.contentHeight
|
|
end
|
|
m:Hide()
|
|
m.displayed = nil
|
|
return 0
|
|
end
|
|
|
|
-- space left in WatchFrameLines below this offset
|
|
local function remainingSpace(maxHeight, offset)
|
|
return (maxHeight or 0) - G.modulesTop - G.rowsBottom + (offset or 0)
|
|
end
|
|
|
|
-- ---------- Quests
|
|
-- durations: largest remaining time seen per quest. 3.3.5 only gives the remaining time
|
|
-- (GetQuestTimers), so that value is the timer bar's full duration.
|
|
local Q = { states = {}, seen = nil, durations = {} }
|
|
|
|
-- WatchFrame_DisplayTrackedQuests for the data, QuestObjectiveTracker for the display.
|
|
-- Zone quests: WotLK's LOCAL_MAP_QUESTS is kept in our own table. The client fills it only in
|
|
-- its quest handler, which is removed; writing it from the addon would taint it, and with it
|
|
-- the world map that reads it.
|
|
local locales = {}
|
|
T.locales = locales
|
|
T.numItems = 0
|
|
|
|
local function displayQuests(lineFrame, initialOffset, maxHeight, frameWidth)
|
|
local m = T.quests
|
|
begin(m, remainingSpace(maxHeight, initialOffset))
|
|
local money = GetMoney()
|
|
local numWatched = GetNumQuestWatches()
|
|
local nPOI = { numbered = 0, inside = 0, outside = 0 }
|
|
local objects = 0
|
|
local seen = {}
|
|
for w = 1, numWatched do
|
|
local index = GetQuestIndexForWatch(w)
|
|
if index then
|
|
local title, _, _, _, _, _, _, _, questID = GetQuestLogTitle(index)
|
|
seen[questID or title] = true
|
|
end
|
|
end
|
|
local questTimers = {}
|
|
local timers = { GetQuestTimers() }
|
|
for i, seconds in ipairs(timers) do
|
|
local index = GetQuestIndexForTimer(i)
|
|
if index then questTimers[index] = seconds end
|
|
end
|
|
|
|
local selection
|
|
if WorldMapFrame and WorldMapFrame:IsShown() then
|
|
selection = WORLDMAP_SETTINGS.selectedQuestId
|
|
else
|
|
table.wipe(locales)
|
|
locales["zone"] = GetCurrentMapZone()
|
|
for id in pairs(CURRENT_MAP_QUESTS) do
|
|
locales[id] = true
|
|
end
|
|
end
|
|
table.wipe(VISIBLE_WATCHES)
|
|
|
|
for w = 1, numWatched do
|
|
local index = GetQuestIndexForWatch(w)
|
|
if index then
|
|
local title, level, questTag, _, _, _, isComplete, _, questID = GetQuestLogTitle(index)
|
|
local required = GetQuestLogRequiredMoney(index)
|
|
local numObjectives = GetNumQuestLeaderBoards(index)
|
|
local failed = isComplete and isComplete < 0
|
|
if failed then
|
|
isComplete = false
|
|
elseif isComplete and isComplete > 0 then
|
|
isComplete = true
|
|
elseif numObjectives == 0 and money >= required then
|
|
isComplete = true
|
|
else
|
|
isComplete = false
|
|
end
|
|
-- WotLK filters
|
|
local keep = true
|
|
if isComplete and bit.band(WATCHFRAME_FILTER_TYPE, WATCHFRAME_FILTER_COMPLETED_QUESTS) ~= WATCHFRAME_FILTER_COMPLETED_QUESTS then
|
|
keep = false
|
|
elseif bit.band(WATCHFRAME_FILTER_TYPE, WATCHFRAME_FILTER_REMOTE_ZONES) ~= WATCHFRAME_FILTER_REMOTE_ZONES and not locales[questID] then
|
|
keep = false
|
|
end
|
|
if keep then
|
|
if required > 0 then WatchFrame.watchMoney = true end
|
|
local _, object, charges = GetQuestLogSpecialItemInfo(index)
|
|
local key = questID or title
|
|
local before = Q.states[key]
|
|
local state = { finished = {}, isComplete = isComplete }
|
|
local b = acquireBlock(m, key)
|
|
b.index, b.watch, b.questID, b.questTitle = index, w, questID, title
|
|
-- quest item, right of the block
|
|
local button
|
|
if object and not isComplete then
|
|
objects = objects + 1
|
|
button = T.object(objects, lineFrame, index, object, charges)
|
|
b.right = -(G.itemSide + G.rightGap)
|
|
end
|
|
setBlockTitle(b, trackerTitle(title, level, questTag))
|
|
|
|
local animate = {}
|
|
if isComplete then
|
|
-- QUEST_LOG_UPDATE: objectives already shown fade out (FadeOutAnim: 1 s then 0.1 s),
|
|
-- then the completion text
|
|
local fade = before and not before.isComplete and not (before.fade and before.fade <= GetTime())
|
|
if fade then
|
|
state.fade = before.fade or (GetTime() + 1.1)
|
|
for j = 1, numObjectives do
|
|
local text = GetQuestLogLeaderBoard(j, index)
|
|
if text then
|
|
local l = row(b, j, WatchFrame_ReverseQuestObjective(text), false, COLOR.finished)
|
|
state.finished[j] = true
|
|
table.insert(animate, { l, not (before.finished and before.finished[j]) })
|
|
play({ l, "fade" }, math.max(0, state.fade - 0.1 - GetTime()), 0.1,
|
|
function(p) l:SetAlpha(1 - p) end)
|
|
end
|
|
end
|
|
state.isComplete = false
|
|
T.restart(state.fade)
|
|
else
|
|
local completionText = GetQuestLogCompletionText(index)
|
|
if completionText then
|
|
row(b, "QuestComplete", completionText, false)
|
|
else
|
|
row(b, "QuestComplete", TEXT.ready, false, COLOR.finished)
|
|
end
|
|
end
|
|
elseif failed then
|
|
row(b, "Failed", FAILED, false, COLOR.failed)
|
|
else
|
|
for j = 1, numObjectives do
|
|
local text, _, finished = GetQuestLogLeaderBoard(j, index)
|
|
if text then
|
|
text = WatchFrame_ReverseQuestObjective(text)
|
|
if finished then
|
|
state.finished[j] = true
|
|
local l = row(b, j, text, false, COLOR.finished)
|
|
table.insert(animate, { l, before and not (before.finished and before.finished[j]) })
|
|
else
|
|
local l = row(b, j, text, true)
|
|
l.checkMark:Hide()
|
|
end
|
|
end
|
|
end
|
|
if required > money then
|
|
row(b, "Money", GetMoneyString(money) .. " / " .. GetMoneyString(required), true)
|
|
end
|
|
local rest = questTimers[index]
|
|
if rest then
|
|
local duration = math.max(Q.durations[key] or 0, rest)
|
|
Q.durations[key] = duration
|
|
timer(b, duration, GetTime() - (duration - rest))
|
|
end
|
|
end
|
|
|
|
Q.states[key] = state
|
|
if place(m, b) then
|
|
if not settings().collapsedModules[m.key] then
|
|
table.insert(VISIBLE_WATCHES, index)
|
|
for _, a in ipairs(animate) do checkOff(a[1], a[2]) end
|
|
if button then
|
|
button:ClearAllPoints()
|
|
button:SetPoint("TOPRIGHT", b, "TOPRIGHT", 0, 0)
|
|
button:Show()
|
|
end
|
|
-- WotLK POI button (QuestPOI_DisplayButton, 32 x 32), centered where camelot centers
|
|
-- its 20 x 20 one
|
|
if WatchFrame.showObjectives then
|
|
local poi
|
|
if CURRENT_MAP_QUESTS[questID] then
|
|
if isComplete then
|
|
nPOI.inside = nPOI.inside + 1
|
|
poi = QuestPOI_DisplayButton("WatchFrameLines", QUEST_POI_COMPLETE_IN, nPOI.inside, questID)
|
|
else
|
|
nPOI.numbered = nPOI.numbered + 1
|
|
poi = QuestPOI_DisplayButton("WatchFrameLines", QUEST_POI_NUMERIC, nPOI.numbered, questID)
|
|
end
|
|
elseif isComplete then
|
|
nPOI.outside = nPOI.outside + 1
|
|
poi = QuestPOI_DisplayButton("WatchFrameLines", QUEST_POI_COMPLETE_OUT, nPOI.outside, questID)
|
|
end
|
|
if poi then
|
|
poi:ClearAllPoints()
|
|
poi:SetPoint("CENTER", b.title, "TOPLEFT", G.poiX, G.poiY)
|
|
end
|
|
end
|
|
-- a quest absent from the previous pass is new (3.3.5 has no QUEST_WATCH_LIST_CHANGED)
|
|
if Q.seen and not Q.seen[key] then
|
|
animateAdd(b)
|
|
end
|
|
elseif button then
|
|
button:Hide()
|
|
end
|
|
else
|
|
if button then
|
|
button:Hide()
|
|
objects = objects - 1
|
|
end
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
for i = objects + 1, T.numItems do
|
|
local it = _G["WatchFrameItem" .. i]
|
|
if it then it:Hide() end
|
|
end
|
|
QuestPOI_HideButtons("WatchFrameLines", QUEST_POI_NUMERIC, nPOI.numbered + 1)
|
|
QuestPOI_HideButtons("WatchFrameLines", QUEST_POI_COMPLETE_IN, nPOI.inside + 1)
|
|
QuestPOI_HideButtons("WatchFrameLines", QUEST_POI_COMPLETE_OUT, nPOI.outside + 1)
|
|
if selection then
|
|
QuestPOI_SelectButtonByQuestId("WatchFrameLines", selection, true)
|
|
end
|
|
-- remember this pass; the first pass animates no quest (they were already there)
|
|
for key in pairs(Q.states) do
|
|
if not seen[key] then Q.states[key] = nil end
|
|
end
|
|
Q.seen = seen
|
|
local height = finalize(m, lineFrame, initialOffset)
|
|
return height, G.width, numWatched
|
|
end
|
|
T.displayQuests = displayQuests
|
|
|
|
-- one more pass when a fade ends
|
|
function T.restart(when)
|
|
play({ T, "rerun" }, math.max(0, when - GetTime()), 0, function() end, function()
|
|
WatchFrame_Update()
|
|
end)
|
|
end
|
|
|
|
-- WatchFrameItem<n>: the WotLK buttons, reskinned once. n: button number;
|
|
-- index: quest log index; icon, charges: the quest item
|
|
function T.object(n, lineFrame, index, icon, charges)
|
|
local b = _G["WatchFrameItem" .. n]
|
|
if not b then
|
|
b = CreateFrame("Button", "WatchFrameItem" .. n, lineFrame, "WatchFrameItemButtonTemplate")
|
|
end
|
|
-- button count: WotLK's WATCHFRAME_NUM_ITEMS is read only by its removed quest handler;
|
|
-- kept here so the addon never writes that global
|
|
if n > T.numItems then T.numItems = n end
|
|
if not b.foreverSkinApplied then
|
|
b.foreverSkinApplied = true
|
|
b:SetWidth(G.itemSide)
|
|
b:SetHeight(G.itemSide)
|
|
local e = ForeverUI.AtlasEntry("ui-questtrackerbutton-questitem-frame")
|
|
b:SetNormalTexture(e and e[1] or "")
|
|
local t = b:GetNormalTexture()
|
|
ForeverUI.SetAtlas(t, "ui-questtrackerbutton-questitem-frame", true)
|
|
t:ClearAllPoints()
|
|
t:SetWidth(G.itemFrame)
|
|
t:SetHeight(G.itemFrame)
|
|
t:SetPoint("CENTER", b, "CENTER", 0, 0)
|
|
b:SetPushedTexture(e and e[1] or "")
|
|
local p = b:GetPushedTexture()
|
|
ForeverUI.SetAtlas(p, "ui-questtrackerbutton-questitem-frame", true)
|
|
p:ClearAllPoints()
|
|
p:SetWidth(G.itemFrame)
|
|
p:SetHeight(G.itemFrame)
|
|
p:SetPoint("CENTER", b, "CENTER", 0, 0)
|
|
end
|
|
b:SetID(index)
|
|
SetItemButtonTexture(b, icon)
|
|
SetItemButtonCount(b, charges)
|
|
b.charges = charges
|
|
WatchFrameItem_UpdateCooldown(b)
|
|
b.rangeTimer = -1
|
|
return b
|
|
end
|
|
|
|
-- ---------- Achievements
|
|
local function showAchievements(lineFrame, initialOffset, maxHeight, frameWidth, ...)
|
|
local m = T.achievements
|
|
begin(m, remainingSpace(maxHeight, initialOffset))
|
|
local count = select("#", ...)
|
|
local arena = ArenaEnemyFrames and ArenaEnemyFrames:IsShown()
|
|
if bit.band(WATCHFRAME_FILTER_TYPE, WATCHFRAME_FILTER_ACHIEVEMENTS) == WATCHFRAME_FILTER_ACHIEVEMENTS then
|
|
for i = 1, count do
|
|
local id = select(i, ...)
|
|
local category = GetAchievementCategory(id)
|
|
local _, name, _, completed, _, _, _, description = GetAchievementInfo(id)
|
|
if not completed and not arena or category == WATCHFRAME_ACHIEVEMENT_ARENA_CATEGORY then
|
|
local b = acquireBlock(m, id)
|
|
b.achievement = id
|
|
setBlockTitle(b, name)
|
|
local numCriteria = GetAchievementNumCriteria(id)
|
|
if numCriteria > 0 then
|
|
local shownItems = 0
|
|
for j = 1, numCriteria do
|
|
local text, _, filled, _, _, _, flags, _, quantity, criteriaID = GetAchievementCriteriaInfo(id, j)
|
|
if filled or shownItems > G.maxCriteria then
|
|
-- skip: criterion done, or past the limit
|
|
elseif shownItems == G.maxCriteria and numCriteria > G.maxCriteria + 1 then
|
|
row(b, "Extra", "...", false)
|
|
shownItems = shownItems + 1
|
|
else
|
|
if bit.band(flags, ACHIEVEMENT_CRITERIA_PROGRESS_BAR) == ACHIEVEMENT_CRITERIA_PROGRESS_BAR then
|
|
text = quantity
|
|
end
|
|
row(b, j, text, true)
|
|
shownItems = shownItems + 1
|
|
local stopwatch = WATCHFRAME_TIMEDCRITERIA[criteriaID]
|
|
if stopwatch and GetTime() - stopwatch.startTime < stopwatch.duration then
|
|
timer(b, stopwatch.duration, stopwatch.startTime)
|
|
end
|
|
end
|
|
end
|
|
else
|
|
row(b, 1, description, true)
|
|
for _, stopwatch in pairs(WATCHFRAME_TIMEDCRITERIA) do
|
|
if stopwatch.achievementID == id and GetTime() - stopwatch.startTime <= stopwatch.duration then
|
|
timer(b, stopwatch.duration, stopwatch.startTime)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if not place(m, b) then
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
local height = finalize(m, lineFrame, initialOffset)
|
|
return height, G.width, count
|
|
end
|
|
|
|
local function achievementHandler(lineFrame, initialOffset, maxHeight, frameWidth)
|
|
return showAchievements(lineFrame, initialOffset, maxHeight, frameWidth, GetTrackedAchievements())
|
|
end
|
|
T.achievementHandler = achievementHandler
|
|
|
|
-- ---------- Clicks
|
|
-- QuestObjectiveTrackerMixin:OnBlockHeaderClick
|
|
local function questClick(b, mouse)
|
|
if IsModifiedClick("CHATLINK") and ChatEdit_GetActiveWindow and ChatEdit_GetActiveWindow() then
|
|
local link = GetQuestLink(b.index)
|
|
if link then ChatEdit_InsertLink(link) end
|
|
return
|
|
end
|
|
if mouse ~= "RightButton" then
|
|
CloseDropDownMenus()
|
|
if IsModifiedClick("QUESTWATCHTOGGLE") then
|
|
WatchFrame_StopTrackingQuest(nil, b.watch)
|
|
else
|
|
T.openPage(b.watch)
|
|
end
|
|
return
|
|
end
|
|
local w = b.watch
|
|
local index = b.index
|
|
local l = {
|
|
{ text = b.questTitle, isTitle = true },
|
|
{ text = TEXT.viewPage, func = function() T.openPage(w) end },
|
|
{ text = TEXT.viewMap, func = function() WatchFrame_OpenMapToQuest(nil, w) end },
|
|
{ text = TEXT.untrack, func = function() WatchFrame_StopTrackingQuest(nil, w) end },
|
|
}
|
|
if GetQuestLogPushable and (GetNumPartyMembers() > 0 or GetNumRaidMembers() > 1) then
|
|
local selection = GetQuestLogSelection()
|
|
SelectQuestLogEntry(index)
|
|
local shareable = GetQuestLogPushable()
|
|
SelectQuestLogEntry(selection)
|
|
if shareable then
|
|
table.insert(l, { text = SHARE_QUEST, func = function() WatchFrame_ShareQuest(nil, w) end })
|
|
end
|
|
end
|
|
-- camelot 70450 drops "Share in chat" outside the gamepad UI; kept on the user's request
|
|
-- (2026-10-18)
|
|
table.insert(l, { text = TEXT.shareInChat, func = function()
|
|
local link = GetQuestLink(index)
|
|
if link and not (ChatEdit_InsertLink and ChatEdit_InsertLink(link)) then
|
|
ChatFrame_OpenChat(link)
|
|
end
|
|
end })
|
|
table.insert(l, { text = TEXT.abandonQuest, func = function() WatchFrame_AbandonQuest(nil, w) end })
|
|
-- WotLK manual ordering
|
|
local n = #VISIBLE_WATCHES
|
|
local rank = WatchFrame_GetVisibleIndex(index)
|
|
if n > 1 and rank then
|
|
if rank > 1 then
|
|
table.insert(l, { text = TRACKER_SORT_MANUAL_UP, func = function() WatchFrame_MoveQuest(nil, index, -1) end })
|
|
table.insert(l, { text = TRACKER_SORT_MANUAL_TOP, func = function() WatchFrame_MoveQuest(nil, index, -100) end })
|
|
end
|
|
if rank < n then
|
|
table.insert(l, { text = TRACKER_SORT_MANUAL_DOWN, func = function() WatchFrame_MoveQuest(nil, index, 1) end })
|
|
table.insert(l, { text = TRACKER_SORT_MANUAL_BOTTOM, func = function() WatchFrame_MoveQuest(nil, index, 100) end })
|
|
end
|
|
end
|
|
menu("cursor", l)
|
|
end
|
|
|
|
-- AchievementObjectiveTrackerMixin:OnBlockHeaderClick
|
|
local function achievementClick(b, mouse)
|
|
local id = b.achievement
|
|
if IsModifiedClick("CHATLINK") and ChatEdit_GetActiveWindow and ChatEdit_GetActiveWindow() then
|
|
local link = GetAchievementLink(id)
|
|
if link then ChatEdit_InsertLink(link) end
|
|
return
|
|
end
|
|
if mouse ~= "RightButton" then
|
|
CloseDropDownMenus()
|
|
if IsModifiedClick("QUESTWATCHTOGGLE") then
|
|
WatchFrame_StopTrackingAchievement(nil, id)
|
|
else
|
|
WatchFrame_OpenAchievementFrame(nil, id)
|
|
end
|
|
return
|
|
end
|
|
local _, name = GetAchievementInfo(id)
|
|
menu("cursor", {
|
|
{ text = name, isTitle = true },
|
|
{ text = TEXT.viewAchievement, func = function() WatchFrame_OpenAchievementFrame(nil, id) end },
|
|
{ text = TEXT.untrack, func = function() WatchFrame_StopTrackingAchievement(nil, id) end },
|
|
})
|
|
end
|
|
|
|
-- QuestMapFrame_OpenToQuestDetails: map, side panel, quest page. WotLK first expands the
|
|
-- quest's header (WatchFrameLinkButtonTemplate_OnLeftClick).
|
|
function T.openPage(watch)
|
|
local index = GetQuestIndexForWatch(watch)
|
|
if not index then return end
|
|
ExpandQuestHeader(GetQuestSortIndex(index))
|
|
index = GetQuestIndexForWatch(watch)
|
|
if index and ForeverUI.QuestLog and ForeverUI.QuestLog.openPage then
|
|
ForeverUI.QuestLog.openPage(index)
|
|
end
|
|
end
|
|
|
|
-- ---------- Assembly
|
|
local function suppressWotLKHeader()
|
|
for _, f in ipairs({ WatchFrameHeader, WatchFrameCollapseExpandButton }) do
|
|
if f then
|
|
f:SetAlpha(0)
|
|
f:EnableMouse(false)
|
|
f:Hide()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- After each WatchFrame_Update our header replaces WotLK's. WotLK decides whether to show it
|
|
-- only in WatchFrame_Update, so that decision is kept: its header is already hidden when
|
|
-- WatchFrame_Collapse / _Expand call us again. fromUpdate: called from WatchFrame_Update.
|
|
local function afterUpdate(fromUpdate)
|
|
local h = T.header
|
|
if not h then return end
|
|
if fromUpdate then
|
|
T.headerWanted = WatchFrameHeader:IsShown() and true or false
|
|
end
|
|
local showRegion = T.headerWanted
|
|
suppressWotLKHeader()
|
|
if showRegion then
|
|
h:Show()
|
|
local active = WatchFrameCollapseExpandButton:IsEnabled() == 1
|
|
if active then h.shrink:Enable() else h.shrink:Disable() end
|
|
else
|
|
h:Hide()
|
|
end
|
|
if WatchFrame.collapsed then
|
|
buttonStates(h.shrink, "ui-questtrackerbutton-expand-all", "ui-questtrackerbutton-expand-all-pressed")
|
|
else
|
|
buttonStates(h.shrink, "ui-questtrackerbutton-collapse-all", "ui-questtrackerbutton-collapse-all-pressed")
|
|
end
|
|
end
|
|
T.afterUpdate = afterUpdate
|
|
|
|
-- WatchFrame_SetWidth and WatchFrame_Collapse / _Expand: 260, collapsed or not.
|
|
-- WATCHFRAME_EXPANDEDWIDTH and WATCHFRAME_MAXLINEWIDTH keep the client's values: the width
|
|
-- is re-applied after it (hooks below), and our handlers ignore the width it passes.
|
|
local function width()
|
|
WatchFrame:SetWidth(G.width)
|
|
end
|
|
|
|
-- The tracker follows a CARRIER registered in edit mode. WotLK's
|
|
-- UIParent_ManageFramePositions re-anchors WatchFrame under MinimapCluster on every pass and
|
|
-- adds a BOTTOMRIGHT point, so WatchFrame is re-placed after it. Its height runs from the
|
|
-- carrier down to WotLK's bottom (CONTAINER_OFFSET_Y, above the action bars).
|
|
function T.place()
|
|
local carrier = T.carrier
|
|
if not carrier then return end
|
|
WatchFrame:ClearAllPoints()
|
|
WatchFrame:SetPoint("TOPLEFT", carrier, "TOPLEFT", 0, 0)
|
|
local top = carrier:GetTop()
|
|
if top then
|
|
WatchFrame:SetHeight(math.max(G.minHeight, top - (CONTAINER_OFFSET_Y or 0)))
|
|
end
|
|
end
|
|
|
|
local function buildCarrier()
|
|
local carrier = CreateFrame("Frame", "ForeverUIObjectiveTrackerHolder", UIParent)
|
|
carrier:SetWidth(G.width)
|
|
carrier:SetHeight(G.headerH)
|
|
T.carrier = carrier
|
|
local L = ForeverUI.Layout
|
|
if L and L.Register then
|
|
L.Register(carrier, "tracking", ForeverUI.L.OBJECTIVETRACKER_EDIT_LABEL, "TOPRIGHT", "TOPRIGHT", G.defaultX, G.defaultY)
|
|
-- moved, reset or applied at login: the tracker follows
|
|
local function after(id)
|
|
if id == "tracking" then T.place() end
|
|
end
|
|
hooksecurefunc(L, "Save", after)
|
|
hooksecurefunc(L, "Apply", after)
|
|
end
|
|
if UIParent_ManageFramePositions then
|
|
hooksecurefunc("UIParent_ManageFramePositions", T.place)
|
|
end
|
|
T.place()
|
|
end
|
|
|
|
local function build()
|
|
if T.header or not WatchFrame or not WatchFrameLines then
|
|
return
|
|
end
|
|
buildCarrier()
|
|
T.header = createHeader()
|
|
T.quests = createModule("ForeverUIQuestObjectiveTracker", TEXT.quests, "quests")
|
|
T.quests.onClick = questClick
|
|
T.achievements = createModule("ForeverUIAchievementObjectiveTracker", TEXT.achievements, "achievements")
|
|
T.achievements.onClick = achievementClick
|
|
|
|
WatchFrameLines:ClearAllPoints()
|
|
WatchFrameLines:SetPoint("TOPLEFT", WatchFrame, "TOPLEFT", 0, -G.modulesTop)
|
|
WatchFrameLines:SetPoint("BOTTOMRIGHT", WatchFrame, "BOTTOMRIGHT", 0, G.rowsBottom)
|
|
width()
|
|
|
|
-- camelot's two modules, in camelot's order, replace the three WotLK handlers;
|
|
-- addon handlers stay after them
|
|
WatchFrame_RemoveObjectiveHandler(WatchFrame_HandleDisplayQuestTimers)
|
|
WatchFrame_RemoveObjectiveHandler(WatchFrame_HandleDisplayTrackedAchievements)
|
|
WatchFrame_RemoveObjectiveHandler(WatchFrame_DisplayTrackedQuests)
|
|
table.insert(WATCHFRAME_OBJECTIVEHANDLERS, 1, displayQuests)
|
|
table.insert(WATCHFRAME_OBJECTIVEHANDLERS, 2, achievementHandler)
|
|
|
|
hooksecurefunc("WatchFrame_Update", function() afterUpdate(true) end)
|
|
hooksecurefunc("WatchFrame_SetWidth", function()
|
|
if not WatchFrame.collapsed then width() end
|
|
end)
|
|
-- Tracking a quest from the map: WotLK adds it to LOCAL_MAP_QUESTS when that table is the
|
|
-- shown zone's, and removes it otherwise (WorldMapFrame.lua:2162-2167). The table is ours,
|
|
-- so do the same, then redo the refresh the client already ran without it.
|
|
if WorldMapTrackQuest_Toggle then
|
|
hooksecurefunc("WorldMapTrackQuest_Toggle", function(checkMark)
|
|
local id = WORLDMAP_SETTINGS and WORLDMAP_SETTINGS.selectedQuestId
|
|
if not id then return end
|
|
if checkMark then
|
|
if locales["zone"] == GetCurrentMapZone() then locales[id] = true end
|
|
else
|
|
locales[id] = nil
|
|
end
|
|
WatchFrame_Update()
|
|
end)
|
|
end
|
|
hooksecurefunc("WatchFrame_Collapse", function(self)
|
|
self:SetWidth(G.width)
|
|
afterUpdate()
|
|
end)
|
|
hooksecurefunc("WatchFrame_Expand", function(self)
|
|
self:SetWidth(G.width)
|
|
afterUpdate()
|
|
end)
|
|
suppressWotLKHeader()
|
|
-- WatchFrame_Update measures the frame, so not before it is placed (it is not at addon
|
|
-- load; its login events refresh it)
|
|
if WatchFrame:GetTop() and WatchFrame:GetBottom() then
|
|
WatchFrame_Update()
|
|
end
|
|
end
|
|
T.build = build
|
|
|
|
build()
|