mirror of
https://github.com/rivi-s/pfQuest.git
synced 2026-11-10 20:17:36 +00:00
1042 lines
35 KiB
Lua
1042 lines
35 KiB
Lua
-- multi api compat
|
|
local compat = pfQuestCompat
|
|
|
|
-- Performance: cache frequently-used globals
|
|
local pairs = pairs
|
|
local min, max = math.min, math.max
|
|
local getn, sort = table.getn, table.sort
|
|
local GetTime = GetTime
|
|
|
|
local fontsize = 12
|
|
local panelheight = 16
|
|
local entryheight = 20
|
|
|
|
-- The server and bundled database can use visually identical punctuation
|
|
-- with different byte sequences (for example straight versus curly
|
|
-- apostrophes). Use a display-independent key so one active quest cannot
|
|
-- occupy two tracker rows through its live and database titles.
|
|
local function TrackerTitleKey(title)
|
|
-- Lua's %w classification depends on the client's character locale. A byte
|
|
-- of a UTF-8 apostrophe can be treated as a letter in single-byte locales,
|
|
-- so replace whole punctuation sequences before stripping other separators.
|
|
title = string.gsub(title or "", "\226\128[\152\153]", "'")
|
|
title = string.gsub(title, "\202\188", "'")
|
|
title = string.gsub(title, "\239\188\135", "'")
|
|
return string.lower(string.gsub(title, "[^%w]", ""))
|
|
end
|
|
|
|
-- Some clients omit collapsed quests from the list; others still return them.
|
|
-- Support both layouts without removing anything from pfQuest's quest cache.
|
|
local function ReadQuestLogVisibility()
|
|
local visible, signature = {}, {}
|
|
local hidden, hasCollapsed = false, false
|
|
for i = 1, GetNumQuestLogEntries() do
|
|
local title, _, _, header, collapsed = compat.GetQuestLogTitle(i)
|
|
if title then
|
|
if header then
|
|
hidden = collapsed == true or collapsed == 1
|
|
if hidden then hasCollapsed = true end
|
|
elseif not hidden then
|
|
visible[title] = true
|
|
end
|
|
table.insert(signature, title .. ":" .. tostring(header) .. ":" .. tostring(collapsed))
|
|
end
|
|
end
|
|
return hasCollapsed and visible or nil, table.concat(signature, "\n")
|
|
end
|
|
|
|
local function HideTooltip()
|
|
GameTooltip:Hide()
|
|
end
|
|
|
|
local function ShowTooltip()
|
|
if this.tooltip then
|
|
GameTooltip:ClearLines()
|
|
GameTooltip_SetDefaultAnchor(GameTooltip, this)
|
|
if this.text then
|
|
GameTooltip:SetText(this.text:GetText())
|
|
GameTooltip:SetText(this.text:GetText(), this.text:GetTextColor())
|
|
else
|
|
GameTooltip:SetText("|cff33ffccpf|cffffffffQuest")
|
|
end
|
|
|
|
if this.node and this.node.questid then
|
|
if
|
|
pfDB["quests"]
|
|
and pfDB["quests"]["loc"]
|
|
and pfDB["quests"]["loc"][this.node.questid]
|
|
and pfDB["quests"]["loc"][this.node.questid]["O"]
|
|
then
|
|
GameTooltip:AddLine(pfDatabase:FormatQuestText(pfDB["quests"]["loc"][this.node.questid]["O"]), 1, 1, 1, 1)
|
|
GameTooltip:AddLine(" ")
|
|
end
|
|
|
|
local qlogid = pfQuest.questlog[this.node.questid] and pfQuest.questlog[this.node.questid].qlogid
|
|
if qlogid then
|
|
local objectives = GetNumQuestLeaderBoards(qlogid)
|
|
if objectives and objectives > 0 then
|
|
for i = 1, objectives, 1 do
|
|
local text, _, done = GetQuestLogLeaderBoard(i, qlogid)
|
|
local _, _, obj, cur, req = strfind(gsub(text, "\239\188\154", ":"), "(.*):%s*([%d]+)%s*/%s*([%d]+)")
|
|
if done then
|
|
GameTooltip:AddLine(" - " .. text, 0, 1, 0)
|
|
elseif cur and req then
|
|
local r, g, b = pfMap.tooltip:GetColor(cur, req)
|
|
GameTooltip:AddLine(" - " .. text, r, g, b)
|
|
else
|
|
GameTooltip:AddLine(" - " .. text, 1, 0, 0)
|
|
end
|
|
end
|
|
GameTooltip:AddLine(" ")
|
|
end
|
|
end
|
|
end
|
|
|
|
GameTooltip:AddLine(this.tooltip, 1, 1, 1)
|
|
GameTooltip:Show()
|
|
end
|
|
end
|
|
|
|
local expand_states = {}
|
|
local manual_expand_states = {}
|
|
|
|
-- Ported from pfQuest-2: level-first remains the default, with an optional
|
|
-- nearest-objective order selected from the tracker header.
|
|
local function GetQuestSortMode()
|
|
return pfQuest_config["trackerquestsort"] == "distance" and "distance" or "level"
|
|
end
|
|
|
|
local function IsLevelSortAscending()
|
|
return pfQuest_config["trackerquestsortreverse"] == "1"
|
|
end
|
|
|
|
local DIST_FAR = 99999999
|
|
|
|
local function UpdateSortButton()
|
|
if not tracker or not tracker.btnsort then return end
|
|
if tracker.mode == "QUEST_TRACKING" then
|
|
tracker.btnsort:Show()
|
|
if GetQuestSortMode() == "distance" then
|
|
tracker.btnsort.label:SetText("N")
|
|
tracker.btnsort.tooltip = "Quest Sort: Nearest First\n|cff33ffcc<Click>|r Sort By Level"
|
|
else
|
|
tracker.btnsort.label:SetText("L")
|
|
local direction = IsLevelSortAscending() and "Low to High" or "High to Low"
|
|
tracker.btnsort.tooltip = "Quest Sort: Level (" .. direction
|
|
.. ")\n|cff33ffcc<Click>|r Sort By Nearest\n|cff33ffcc<Right-Click>|r Reverse Level Order"
|
|
end
|
|
else
|
|
tracker.btnsort:Hide()
|
|
end
|
|
end
|
|
|
|
local function UpdateQuestDistances()
|
|
if tracker.mode ~= "QUEST_TRACKING" or GetQuestSortMode() ~= "distance" then return end
|
|
|
|
local changed, nearestByTitle, nearestByID = nil, {}, {}
|
|
local xplayer, yplayer = GetPlayerMapPosition("player")
|
|
if xplayer ~= 0 or yplayer ~= 0 then
|
|
for _, point in ipairs(tracker.questPoints or {}) do
|
|
local x, y = (xplayer * 100 - point.x) * 1.5, yplayer * 100 - point.y
|
|
local distance = ceil(math.sqrt(x * x + y * y) * 100) / 100
|
|
if not nearestByTitle[point.title] or distance < nearestByTitle[point.title] then
|
|
nearestByTitle[point.title] = distance
|
|
end
|
|
if point.questid and (not nearestByID[point.questid] or distance < nearestByID[point.questid]) then
|
|
nearestByID[point.questid] = distance
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Older maps and third-party pins can have no tracker point. Retain route
|
|
-- points as a fallback, but never let route display settings decide the
|
|
-- nearest order for ordinary active quest objectives.
|
|
for _, data in ipairs((pfQuest.route and pfQuest.route.coords) or {}) do
|
|
local pin, distance = data[3], data[4]
|
|
if pin and pin.node and distance then
|
|
for title, node in pairs(pin.node) do
|
|
if not nearestByTitle[title] then
|
|
nearestByTitle[title] = distance
|
|
end
|
|
if node.questid and not nearestByID[node.questid] then
|
|
nearestByID[node.questid] = distance
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
for _, button in pairs(tracker.buttons) do
|
|
if not button.empty then
|
|
local distance = nearestByID[button.questid] or nearestByTitle[button.title]
|
|
if button.distance ~= distance then
|
|
button.distance = distance
|
|
changed = true
|
|
end
|
|
elseif button.distance then
|
|
button.distance = nil
|
|
end
|
|
end
|
|
if changed then
|
|
tracker.needsSort = true
|
|
-- Distance mode is live: request a deferred layout after movement changes
|
|
-- the nearest objective, rather than waiting for the next map refresh.
|
|
tracker:ScheduleLayout()
|
|
end
|
|
end
|
|
|
|
tracker = CreateFrame("Frame", "pfQuestMapTracker", UIParent)
|
|
tracker:Hide()
|
|
tracker:SetPoint("LEFT", UIParent, "LEFT", 0, 0)
|
|
tracker:SetWidth(200)
|
|
tracker:SetMovable(true)
|
|
tracker:EnableMouse(true)
|
|
tracker:SetClampedToScreen(true)
|
|
|
|
-- Map rendering registers every active quest coordinate. This intentionally
|
|
-- differs from route.coords: a tracker sort must include objectives even when
|
|
-- their route line, arrow, cluster, or endpoint display is disabled.
|
|
function tracker.RegisterQuestPoint(title, node, x, y)
|
|
local id = tracker.buttonByTitle[TrackerTitleKey(title)]
|
|
local button = id and tracker.buttons[id]
|
|
if not button or button.empty then return end
|
|
|
|
tracker.questPoints = tracker.questPoints or {}
|
|
table.insert(tracker.questPoints, {
|
|
title = title,
|
|
questid = button.questid or node.questid,
|
|
x = x,
|
|
y = y,
|
|
})
|
|
end
|
|
|
|
tracker:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
tracker:SetScript("OnEvent", function()
|
|
-- update font sizes according to config
|
|
fontsize = tonumber(pfQuest_config["trackerfontsize"]) or 12
|
|
entryheight = ceil(fontsize * 1.6)
|
|
|
|
-- restore tracker state
|
|
if pfQuest_config["showtracker"] and pfQuest_config["showtracker"] == "0" then
|
|
this:Hide()
|
|
else
|
|
this:Show()
|
|
end
|
|
end)
|
|
|
|
tracker:SetScript("OnMouseDown", function()
|
|
if not pfQuest_config.lock then
|
|
this:StartMoving()
|
|
end
|
|
end)
|
|
|
|
tracker:SetScript("OnMouseUp", function()
|
|
this:StopMovingOrSizing()
|
|
local anchor, x, y = pfUI.api.ConvertFrameAnchor(this, pfUI.api.GetBestAnchor(this))
|
|
this:ClearAllPoints()
|
|
this:SetPoint(anchor, x, y)
|
|
|
|
-- save position
|
|
pfQuest_config.trackerpos = { anchor, x, y }
|
|
end)
|
|
|
|
tracker:SetScript("OnUpdate", function()
|
|
-- Native objective reads can emit more quest-log events. Keep those events
|
|
-- read-only and coalesce row refreshes outside their dispatch stack.
|
|
if (this.contentTick or 0) <= GetTime() then
|
|
this.contentTick = GetTime() + 0.2
|
|
for _, button in pairs(this.buttons or {}) do
|
|
if button.contentPending then
|
|
button.contentPending = nil
|
|
tracker.ButtonEvent(button)
|
|
end
|
|
end
|
|
end
|
|
-- Objective updates can change an entry's height. Reflow the complete list
|
|
-- once the QUEST_LOG_UPDATE burst settles so entries never retain old offsets.
|
|
if this.layoutAt and this.layoutAt <= GetTime() then
|
|
this.layoutAt = nil
|
|
this:DoLayout()
|
|
end
|
|
|
|
if WorldMapFrame:IsShown() then
|
|
if this.strata ~= "FULLSCREEN_DIALOG" then
|
|
this:SetFrameStrata("FULLSCREEN_DIALOG")
|
|
this.strata = "FULLSCREEN_DIALOG"
|
|
end
|
|
else
|
|
if this.strata ~= "BACKGROUND" then
|
|
this:SetFrameStrata("BACKGROUND")
|
|
this.strata = "BACKGROUND"
|
|
end
|
|
end
|
|
|
|
local alpha = this.backdrop:GetAlpha()
|
|
local content = tracker.buttons[1] and not tracker.buttons[1].empty and true or nil
|
|
local goal = (content and not MouseIsOver(this)) and 0 or not content and not MouseIsOver(this) and 0.5 or 1
|
|
if ceil(alpha * 10) ~= ceil(goal * 10) then
|
|
this.backdrop:SetAlpha(alpha + ((goal - alpha) > 0 and 0.1 or (goal - alpha) < 0 and -0.1 or 0))
|
|
end
|
|
|
|
if pfQuestCompat.QuestWatchFrame:IsShown() then
|
|
pfQuestCompat.QuestWatchFrame:Hide()
|
|
end
|
|
|
|
if tracker.mode == "QUEST_TRACKING" and GetQuestSortMode() == "distance" then
|
|
if not this.distanceTick or this.distanceTick < GetTime() then
|
|
this.distanceTick = GetTime() + 0.2
|
|
UpdateQuestDistances()
|
|
end
|
|
else
|
|
this.distanceTick = nil
|
|
end
|
|
end)
|
|
|
|
-- Section collapse/expand emits QUEST_LOG_UPDATE. Refresh on that event
|
|
-- instead of scanning the full quest log from the map tracker's OnUpdate.
|
|
tracker:RegisterEvent("QUEST_LOG_UPDATE")
|
|
tracker:SetScript("OnEvent", function()
|
|
if event == "PLAYER_ENTERING_WORLD" then
|
|
-- update font sizes according to config
|
|
fontsize = tonumber(pfQuest_config["trackerfontsize"]) or 12
|
|
entryheight = ceil(fontsize * 1.6)
|
|
|
|
-- restore tracker state
|
|
if pfQuest_config["showtracker"] and pfQuest_config["showtracker"] == "0" then
|
|
this:Hide()
|
|
else
|
|
this:Show()
|
|
end
|
|
UpdateSortButton()
|
|
elseif event == "QUEST_LOG_UPDATE" then
|
|
this.sectionSignature = nil
|
|
this:ScheduleLayout()
|
|
end
|
|
end)
|
|
|
|
tracker:SetScript("OnShow", function()
|
|
pfQuest_config["showtracker"] = "1"
|
|
|
|
-- load tracker position if exists
|
|
if pfQuest_config.trackerpos then
|
|
this:ClearAllPoints()
|
|
this:SetPoint(unpack(pfQuest_config.trackerpos))
|
|
end
|
|
end)
|
|
|
|
tracker:SetScript("OnHide", function()
|
|
pfQuest_config["showtracker"] = "0"
|
|
end)
|
|
|
|
tracker.buttons = {}
|
|
tracker.buttonByTitle = {} -- reverse map: title → button index, for O(1) duplicate detection
|
|
tracker.mode = "QUEST_TRACKING"
|
|
|
|
tracker.backdrop = CreateFrame("Frame", nil, tracker)
|
|
tracker.backdrop:SetAllPoints(tracker)
|
|
tracker.backdrop.bg = tracker.backdrop:CreateTexture(nil, "BACKGROUND")
|
|
tracker.backdrop.bg:SetTexture(0, 0, 0, 0.2)
|
|
tracker.backdrop.bg:SetAllPoints()
|
|
|
|
do -- button panel
|
|
tracker.panel = CreateFrame("Frame", nil, tracker.backdrop)
|
|
tracker.panel:SetPoint("TOPLEFT", 0, 0)
|
|
tracker.panel:SetPoint("TOPRIGHT", 0, 0)
|
|
tracker.panel:SetHeight(panelheight)
|
|
|
|
local anchors = {}
|
|
local buttons = {}
|
|
local function CreateButton(icon, anchor, tooltip, func)
|
|
anchors[anchor] = anchors[anchor] and anchors[anchor] + 1 or 0
|
|
local pos = 1 + (panelheight + 1) * anchors[anchor]
|
|
pos = anchor == "TOPLEFT" and pos or pos * -1
|
|
local func = func
|
|
|
|
local b = CreateFrame("Button", nil, tracker.panel)
|
|
b.tooltip = tooltip
|
|
b.icon = b:CreateTexture(nil, "BACKGROUND")
|
|
b.icon:SetAllPoints()
|
|
b.icon:SetTexture(pfQuestConfig.path .. "\\img\\tracker_" .. icon)
|
|
if table.getn(buttons) == 0 then
|
|
b.icon:SetVertexColor(0.2, 1, 0.8)
|
|
end
|
|
|
|
b:SetPoint(anchor, pos, -1)
|
|
b:SetWidth(panelheight - 2)
|
|
b:SetHeight(panelheight - 2)
|
|
|
|
b:SetScript("OnEnter", ShowTooltip)
|
|
b:SetScript("OnLeave", HideTooltip)
|
|
|
|
if anchor == "TOPLEFT" then
|
|
table.insert(buttons, b)
|
|
b:SetScript("OnClick", function()
|
|
if func then
|
|
func()
|
|
end
|
|
for id, button in pairs(buttons) do
|
|
button.icon:SetVertexColor(1, 1, 1)
|
|
end
|
|
this.icon:SetVertexColor(0.2, 1, 0.8)
|
|
end)
|
|
else
|
|
b:SetScript("OnClick", func)
|
|
end
|
|
|
|
return b
|
|
end
|
|
|
|
tracker.btnquest = CreateButton("quests", "TOPLEFT", pfQuest_Loc["Show Current Quests"], function()
|
|
tracker.mode = "QUEST_TRACKING"
|
|
UpdateSortButton()
|
|
pfMap:UpdateNodes()
|
|
end)
|
|
|
|
tracker.btndatabase = CreateButton("database", "TOPLEFT", pfQuest_Loc["Show Database Results"], function()
|
|
tracker.mode = "DATABASE_TRACKING"
|
|
UpdateSortButton()
|
|
pfMap:UpdateNodes()
|
|
end)
|
|
|
|
tracker.btngiver = CreateButton("giver", "TOPLEFT", pfQuest_Loc["Show Quest Givers"], function()
|
|
tracker.mode = "GIVER_TRACKING"
|
|
UpdateSortButton()
|
|
pfMap:UpdateNodes()
|
|
end)
|
|
|
|
tracker.btnsort = CreateFrame("Button", nil, tracker.panel)
|
|
tracker.btnsort:SetPoint("TOPRIGHT", -69, -1)
|
|
tracker.btnsort:SetWidth(panelheight - 2)
|
|
tracker.btnsort:SetHeight(panelheight - 2)
|
|
tracker.btnsort.tooltip = "Quest Sort"
|
|
tracker.btnsort.bg = tracker.btnsort:CreateTexture(nil, "BACKGROUND")
|
|
tracker.btnsort.bg:SetAllPoints()
|
|
tracker.btnsort.bg:SetTexture(0, 0, 0, 0)
|
|
tracker.btnsort.label = tracker.btnsort:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
|
tracker.btnsort.label:SetAllPoints()
|
|
tracker.btnsort.label:SetFont(pfUI.font_default, 11)
|
|
tracker.btnsort.label:SetTextColor(0.9, 0.9, 0.9, 1)
|
|
tracker.btnsort:SetScript("OnEnter", ShowTooltip)
|
|
tracker.btnsort:SetScript("OnLeave", HideTooltip)
|
|
tracker.btnsort:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
|
tracker.btnsort:SetScript("OnClick", function()
|
|
if arg1 == "RightButton" and GetQuestSortMode() == "level" then
|
|
pfQuest_config["trackerquestsortreverse"] = IsLevelSortAscending() and "0" or "1"
|
|
UpdateSortButton()
|
|
tracker.needsSort = true
|
|
tracker:DoLayout()
|
|
return
|
|
end
|
|
|
|
pfQuest_config["trackerquestsort"] = GetQuestSortMode() == "distance" and "level" or "distance"
|
|
UpdateSortButton()
|
|
tracker.needsSort = true
|
|
-- Nearest-first is an explicit action: calculate the current map's
|
|
-- route distances now instead of waiting for the route's movement tick.
|
|
if pfQuest.route and pfQuest.route.UpdateDistances then
|
|
pfQuest.route:UpdateDistances()
|
|
end
|
|
UpdateQuestDistances()
|
|
tracker.distanceTick = GetTime() + 0.2
|
|
tracker:DoLayout()
|
|
end)
|
|
|
|
tracker.btnclose = CreateButton("close", "TOPRIGHT", pfQuest_Loc["Close Tracker"], function()
|
|
DEFAULT_CHAT_FRAME:AddMessage(
|
|
pfQuest_Loc["|cff33ffccpf|cffffffffQuest: Tracker is now hidden. Type `/db tracker` to show."]
|
|
)
|
|
tracker:Hide()
|
|
end)
|
|
|
|
tracker.btnsettings = CreateButton("settings", "TOPRIGHT", pfQuest_Loc["Open Settings"], function()
|
|
if pfQuestConfig then
|
|
pfQuestConfig:Show()
|
|
end
|
|
end)
|
|
|
|
tracker.btnclean = CreateButton("clean", "TOPRIGHT", pfQuest_Loc["Clean Database Results"], function()
|
|
pfMap:DeleteNode("PFDB")
|
|
pfMap:UpdateNodes()
|
|
end)
|
|
|
|
tracker.btnsearch = CreateButton("search", "TOPRIGHT", pfQuest_Loc["Open Database Browser"], function()
|
|
if pfBrowser then
|
|
pfBrowser:Show()
|
|
end
|
|
end)
|
|
end
|
|
|
|
function tracker.ButtonEnter()
|
|
pfMap.highlight = this.title
|
|
tracker.ButtonUpdate(this)
|
|
ShowTooltip()
|
|
end
|
|
|
|
function tracker.ButtonLeave()
|
|
pfMap.highlight = nil
|
|
tracker.ButtonUpdate(this)
|
|
HideTooltip()
|
|
end
|
|
|
|
function tracker.ButtonUpdate(button)
|
|
local button = button or this
|
|
local alpha = tonumber((pfQuest_config["trackeralpha"] or 0.2)) or 0.2
|
|
|
|
if not button.alpha or button.alpha ~= alpha then
|
|
button.bg:SetTexture(0, 0, 0, alpha)
|
|
button.bg:SetAlpha(alpha)
|
|
button.alpha = alpha
|
|
end
|
|
|
|
if pfMap.highlight and pfMap.highlight == button.title then
|
|
if not button.highlight then
|
|
button.bg:SetTexture(1, 1, 1, math.max(0.2, alpha))
|
|
button.bg:SetAlpha(math.max(0.5, alpha))
|
|
button.highlight = true
|
|
end
|
|
elseif button.highlight then
|
|
button.bg:SetTexture(0, 0, 0, alpha)
|
|
button.bg:SetAlpha(alpha)
|
|
button.highlight = nil
|
|
end
|
|
end
|
|
|
|
function tracker.ButtonClick()
|
|
if arg1 == "RightButton" then
|
|
for questid, data in pairs(pfQuest.questlog) do
|
|
if data.title == this.title then
|
|
-- show questlog
|
|
HideUIPanel(QuestLogFrame)
|
|
SelectQuestLogEntry(data.qlogid)
|
|
ShowUIPanel(QuestLogFrame)
|
|
break
|
|
end
|
|
end
|
|
elseif IsShiftKeyDown() then
|
|
-- mark as done if node is quest and not in questlog
|
|
if this.node.questid and not this.node.qlogid then
|
|
-- mark as done in history
|
|
pfQuest_history[this.node.questid] = { time(), UnitLevel("player") }
|
|
UIErrorsFrame:AddMessage(
|
|
string.format("The Quest |cffffcc00[%s]|r (id:%s) is now marked as done.", this.title, this.node.questid),
|
|
1,
|
|
1,
|
|
1
|
|
)
|
|
end
|
|
|
|
pfMap:DeleteNode(this.node.addon, this.title)
|
|
pfMap:UpdateNodes()
|
|
|
|
pfQuest.updateQuestGivers = true
|
|
elseif IsControlKeyDown() and not WorldMapFrame:IsShown() then
|
|
-- show world map
|
|
if ToggleWorldMap then
|
|
-- vanilla & tbc
|
|
ToggleWorldMap()
|
|
else
|
|
-- wotlk
|
|
WorldMapFrame:Show()
|
|
end
|
|
elseif IsControlKeyDown() and pfQuest_config["spawncolors"] == "0" then
|
|
-- switch color
|
|
pfQuest_colors[this.title] = { pfMap.str2rgb(this.title .. GetTime()) }
|
|
pfMap:RefreshPinColor(this.title)
|
|
pfMap:UpdateNodes()
|
|
elseif expand_states[this.title] == 0 then
|
|
expand_states[this.title] = 1
|
|
manual_expand_states[this.title] = true
|
|
tracker.ButtonEvent(this)
|
|
elseif expand_states[this.title] == 1 then
|
|
expand_states[this.title] = 0
|
|
manual_expand_states[this.title] = true
|
|
tracker.ButtonEvent(this)
|
|
end
|
|
end
|
|
|
|
local function trackersort(a, b)
|
|
if a.empty then
|
|
return false
|
|
elseif (a.tracked and 1 or -1) ~= (b.tracked and 1 or -1) then
|
|
return (a.tracked and 1 or -1) > (b.tracked and 1 or -1)
|
|
elseif tracker.mode == "QUEST_TRACKING" and GetQuestSortMode() == "distance"
|
|
and (a.distance or DIST_FAR) ~= (b.distance or DIST_FAR) then
|
|
return (a.distance or DIST_FAR) < (b.distance or DIST_FAR)
|
|
elseif (a.level or -1) ~= (b.level or -1) then
|
|
if tracker.mode == "QUEST_TRACKING" and IsLevelSortAscending() then
|
|
return (a.level or -1) < (b.level or -1)
|
|
end
|
|
return (a.level or -1) > (b.level or -1)
|
|
elseif tracker.mode == "QUEST_TRACKING" and (a.distance or DIST_FAR) ~= (b.distance or DIST_FAR) then
|
|
return (a.distance or DIST_FAR) < (b.distance or DIST_FAR)
|
|
elseif (a.perc or -1) ~= (b.perc or -1) then
|
|
return (a.perc or -1) > (b.perc or -1)
|
|
elseif (a.title or "") ~= (b.title or "") then
|
|
return (a.title or "") < (b.title or "")
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
-- Reusable cache for GetQuestLogLeaderBoard results within a single ButtonEvent
|
|
-- call. Avoids calling the API twice per objective (once for progress, once for
|
|
-- display). Cleared at the start of each ButtonEvent invocation.
|
|
local board_cache = {}
|
|
|
|
function tracker.ButtonEvent(self)
|
|
local self = self or this
|
|
local title = self.title
|
|
local node = self.node
|
|
local id = self.id
|
|
local qid = self.questid
|
|
|
|
self:SetHeight(0)
|
|
|
|
-- we got an event on a hidden button
|
|
if not title then
|
|
return
|
|
end
|
|
if self.empty then
|
|
return
|
|
end
|
|
|
|
self:SetHeight(entryheight)
|
|
|
|
-- initialize and hide all objectives
|
|
self.objectives = self.objectives or {}
|
|
for id, obj in pairs(self.objectives) do
|
|
obj:Hide()
|
|
end
|
|
|
|
-- update button icon
|
|
if node.texture then
|
|
self.icon:SetTexture(node.texture)
|
|
|
|
local r, g, b = unpack(node.vertex or { 0, 0, 0 })
|
|
if r > 0 or g > 0 or b > 0 then
|
|
self.icon:SetVertexColor(unpack(node.vertex))
|
|
else
|
|
self.icon:SetVertexColor(1, 1, 1, 1)
|
|
end
|
|
elseif pfQuest_config["spawncolors"] == "1" then
|
|
self.icon:SetTexture(pfQuestConfig.path .. "\\img\\available_c")
|
|
self.icon:SetVertexColor(1, 1, 1, 1)
|
|
else
|
|
self.icon:SetTexture(pfQuestConfig.path .. "\\img\\node")
|
|
self.icon:SetVertexColor(pfMap.str2rgb(title))
|
|
end
|
|
|
|
if tracker.mode == "QUEST_TRACKING" then
|
|
local qlogid = pfQuest.questlog[qid] and pfQuest.questlog[qid].qlogid or 0
|
|
local qtitle, level, tag, header, collapsed, complete = compat.GetQuestLogTitle(qlogid)
|
|
if not qlogid or not qtitle then
|
|
return
|
|
end
|
|
local objectives = GetNumQuestLeaderBoards(qlogid)
|
|
-- A quest button can retain the node texture captured before the client
|
|
-- reports its final state. Refresh the completion icon from the current
|
|
-- quest log on every event; objective-free talk/report quests are ready
|
|
-- by the same rule used by pfQuest's map tooltip and objective state.
|
|
if complete or objectives == 0 then
|
|
self.icon:SetTexture(pfQuestConfig.path .. "\\img\\complete_c")
|
|
self.icon:SetVertexColor(1, 1, 1, 1)
|
|
end
|
|
local watched = IsQuestWatched(qlogid)
|
|
local color = pfQuestCompat.GetDifficultyColor(level)
|
|
local cur, max = 0, 0
|
|
local percent = 0
|
|
|
|
-- write expand state
|
|
if not expand_states[title] then
|
|
expand_states[title] = pfQuest_config["trackerexpand"] == "1" and 1 or 0
|
|
end
|
|
|
|
local expanded = expand_states[title] == 1 and true or nil
|
|
|
|
if objectives and objectives > 0 then
|
|
-- populate cache and compute progress in one pass
|
|
for i = 1, objectives, 1 do
|
|
local text, type, done = GetQuestLogLeaderBoard(i, qlogid)
|
|
board_cache[i] = { text, type, done }
|
|
local _, _, obj, objNum, objNeeded = strfind(gsub(text, "\239\188\154", ":"), "(.*):%s*([%d]+)%s*/%s*([%d]+)")
|
|
if objNum and objNeeded then
|
|
max = max + objNeeded
|
|
cur = cur + objNum
|
|
elseif not done then
|
|
max = max + 1
|
|
end
|
|
end
|
|
-- clear stale entries beyond current objective count
|
|
for i = objectives + 1, table.getn(board_cache) do
|
|
board_cache[i] = nil
|
|
end
|
|
end
|
|
|
|
if cur == max or complete then
|
|
cur, max = 1, 1
|
|
percent = 100
|
|
else
|
|
percent = cur / max * 100
|
|
end
|
|
|
|
-- expand button to show objectives
|
|
if objectives and (expanded or (percent > 0 and percent < 100)) then
|
|
self:SetHeight(entryheight + objectives * fontsize)
|
|
|
|
for i = 1, objectives, 1 do
|
|
-- read from cache instead of calling GetQuestLogLeaderBoard again
|
|
local entry = board_cache[i]
|
|
local text, _, done = entry[1], entry[2], entry[3]
|
|
local _, _, obj, objNum, objNeeded = strfind(gsub(text, "\239\188\154", ":"), "(.*):%s*([%d]+)%s*/%s*([%d]+)")
|
|
|
|
if not self.objectives[i] then
|
|
self.objectives[i] = self:CreateFontString(nil, "HIGH", "GameFontNormal")
|
|
self.objectives[i]:SetFont(pfUI.font_default, fontsize)
|
|
self.objectives[i]:SetJustifyH("LEFT")
|
|
self.objectives[i]:SetPoint("TOPLEFT", 20, -fontsize * i - 6)
|
|
self.objectives[i]:SetPoint("TOPRIGHT", -10, -fontsize * i - 6)
|
|
end
|
|
|
|
if objNum and objNeeded then
|
|
local r, g, b = pfMap.tooltip:GetColor(objNum, objNeeded)
|
|
self.objectives[i]:SetTextColor(r + 0.2, g + 0.2, b + 0.2)
|
|
self.objectives[i]:SetText(string.format("|cffffffff- %s:|r %s/%s", obj, objNum, objNeeded))
|
|
else
|
|
self.objectives[i]:SetTextColor(0.8, 0.8, 0.8)
|
|
self.objectives[i]:SetText("|cffffffff- " .. text)
|
|
end
|
|
|
|
self.objectives[i]:Show()
|
|
end
|
|
end
|
|
|
|
local r, g, b = pfMap.tooltip:GetColor(cur, max)
|
|
local colorperc = string.format("|cff%02x%02x%02x", r * 255, g * 255, b * 255)
|
|
local showlevel = pfQuest_config["trackerlevel"] == "1" and "[" .. (level or "??") .. (tag and "+" or "") .. "] "
|
|
or ""
|
|
|
|
self.tracked = watched
|
|
-- The tracker sort uses the numeric quest level. Keep it on the quest
|
|
-- button just as giver entries do, otherwise level-first compares every
|
|
-- quest as an unset value.
|
|
self.level = tonumber(level)
|
|
self.perc = percent
|
|
self.text:SetText(
|
|
string.format("%s%s |cffaaaaaa(%s%s%%|cffaaaaaa)|r", showlevel, title or "", colorperc or "", ceil(percent))
|
|
)
|
|
self.text:SetTextColor(color.r, color.g, color.b)
|
|
self.tooltip =
|
|
pfQuest_Loc["|cff33ffcc<Click>|r Unfold/Fold Objectives\n|cff33ffcc<Right-Click>|r Show In QuestLog\n|cff33ffcc<Ctrl-Click>|r Show Map / Toggle Color\n|cff33ffcc<Shift-Click>|r Hide Nodes"]
|
|
elseif tracker.mode == "GIVER_TRACKING" then
|
|
local level = node.qlvl or node.level or UnitLevel("player")
|
|
local color = pfQuestCompat.GetDifficultyColor(level)
|
|
|
|
-- red quests
|
|
if node.qmin and node.qmin > UnitLevel("player") then
|
|
color = { r = 1, g = 0, b = 0 }
|
|
end
|
|
|
|
-- detect daily quests
|
|
if node.qmin and node.qlvl and math.abs(node.qmin - node.qlvl) >= 30 then
|
|
level, color = 0, { r = 0.2, g = 0.8, b = 1 }
|
|
end
|
|
|
|
local showlevel = pfQuest_config["trackerlevel"] == "1" and "[" .. (level or "??") .. "] " or ""
|
|
self.text:SetTextColor(color.r, color.g, color.b)
|
|
self.text:SetText(showlevel .. title)
|
|
self.level = tonumber(level)
|
|
self.tooltip =
|
|
pfQuest_Loc["|cff33ffcc<Ctrl-Click>|r Show Map / Toggle Color\n|cff33ffcc<Shift-Click>|r Mark As Done"]
|
|
elseif tracker.mode == "DATABASE_TRACKING" then
|
|
self.text:SetText(title)
|
|
self.text:SetTextColor(1, 1, 1, 1)
|
|
self.text:SetTextColor(pfMap.str2rgb(title))
|
|
self.tooltip = pfQuest_Loc["|cff33ffcc<Ctrl-Click>|r Show Map / Toggle Color\n|cff33ffcc<Shift-Click>|r Hide Nodes"]
|
|
end
|
|
|
|
-- Mark for sort instead of sorting immediately (deferred)
|
|
tracker.needsSort = true
|
|
tracker:ScheduleLayout()
|
|
if tracker.mode == "QUEST_TRACKING" and tracker.visibleQuests and not tracker.visibleQuests[title] then
|
|
self:Hide()
|
|
else
|
|
self:Show()
|
|
end
|
|
end
|
|
|
|
-- Separate function for layout (only called when needed)
|
|
-- Apply tracker presentation once, outside quest-event dispatch. Rows retain
|
|
-- their bindings; changing appearance never reloads quest or map data.
|
|
function tracker.ApplyAppearance(changes)
|
|
if changes.trackerfontsize then
|
|
local requested = tonumber(pfQuest_config.trackerfontsize)
|
|
fontsize = requested and requested > 0 and requested or 12
|
|
entryheight = ceil(fontsize * 1.6)
|
|
end
|
|
if changes.trackerexpand then
|
|
local expanded = pfQuest_config.trackerexpand == "1" and 1 or 0
|
|
for title in pairs(expand_states) do
|
|
if not manual_expand_states[title] then expand_states[title] = expanded end
|
|
end
|
|
end
|
|
local refreshContent = changes.trackerfontsize or changes.trackerlevel or changes.trackerexpand
|
|
for _, button in pairs(tracker.buttons or {}) do
|
|
if not button.empty and button.title then
|
|
if changes.trackerfontsize then
|
|
button.text:SetFont(pfUI.font_default, fontsize)
|
|
for index, objective in pairs(button.objectives or {}) do
|
|
objective:SetFont(pfUI.font_default, fontsize)
|
|
objective:ClearAllPoints()
|
|
objective:SetPoint("TOPLEFT", button, "TOPLEFT", 20, -fontsize * index - 6)
|
|
objective:SetPoint("TOPRIGHT", button, "TOPRIGHT", -10, -fontsize * index - 6)
|
|
end
|
|
end
|
|
if refreshContent then
|
|
button.contentPending = nil
|
|
tracker.ButtonEvent(button)
|
|
end
|
|
tracker.ButtonUpdate(button)
|
|
end
|
|
end
|
|
tracker.DoLayout()
|
|
end
|
|
|
|
function tracker.DoLayout()
|
|
-- Sort all tracker entries if needed
|
|
if tracker.needsSort then
|
|
sort(tracker.buttons, trackersort)
|
|
tracker.needsSort = nil
|
|
|
|
-- Sorting moves frame objects between numeric slots. Rebuild the reverse
|
|
-- lookup so later updates address the frame that now owns each title.
|
|
for title in pairs(tracker.buttonByTitle) do
|
|
tracker.buttonByTitle[title] = nil
|
|
end
|
|
for id = 1, getn(tracker.buttons) do
|
|
local button = tracker.buttons[id]
|
|
if button and not button.empty and button.title then
|
|
tracker.buttonByTitle[TrackerTitleKey(button.title)] = id
|
|
end
|
|
end
|
|
end
|
|
|
|
local visibleQuests = tracker.mode == "QUEST_TRACKING" and ReadQuestLogVisibility() or nil
|
|
tracker.visibleQuests = visibleQuests
|
|
|
|
-- resize window and align buttons
|
|
local height = panelheight
|
|
local width = 100
|
|
|
|
for bid = 1, getn(tracker.buttons) do
|
|
local button = tracker.buttons[bid]
|
|
button:ClearAllPoints()
|
|
button:SetPoint("TOPRIGHT", tracker, "TOPRIGHT", 0, -height)
|
|
button:SetPoint("TOPLEFT", tracker, "TOPLEFT", 0, -height)
|
|
if not button.empty and (not visibleQuests or visibleQuests[button.title]) then
|
|
button:Show()
|
|
height = height + button:GetHeight()
|
|
|
|
-- Cache GetStringWidth result (avoid calling twice)
|
|
local textWidth = button.text:GetStringWidth()
|
|
if textWidth > width then
|
|
width = textWidth
|
|
end
|
|
|
|
for id, objective in pairs(button.objectives) do
|
|
if objective:IsShown() then
|
|
local objWidth = objective:GetStringWidth()
|
|
if objWidth > width then
|
|
width = objWidth
|
|
end
|
|
end
|
|
end
|
|
else
|
|
button:Hide()
|
|
end
|
|
end
|
|
|
|
width = min(width, 300) + 30
|
|
tracker:SetHeight(height)
|
|
tracker:SetWidth(width)
|
|
end
|
|
|
|
function tracker:ScheduleLayout()
|
|
self.layoutAt = GetTime() + 0.05
|
|
end
|
|
|
|
function tracker.ButtonAdd(title, node)
|
|
if not title or not node then
|
|
return
|
|
end
|
|
|
|
-- O(1) questid lookup: node.questid is set for all PFQUEST nodes from the DB.
|
|
-- For the rare case it's missing or not in questlog (title-keyed quests), fall
|
|
-- back to the linear scan so correctness is preserved.
|
|
local questid = node.questid
|
|
-- Reset() creates dummy entries without a database node. Resolve those by
|
|
-- title too; otherwise the QUEST_TRACKING guard below rejects every dummy
|
|
-- entry before it can display (notably client-unwatched quests).
|
|
if not questid or not pfQuest.questlog[questid] then
|
|
questid = title
|
|
for qid, data in pairs(pfQuest.questlog) do
|
|
if data.title == title then
|
|
questid = qid
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if tracker.mode == "QUEST_TRACKING" then -- skip everything that isn't in questlog
|
|
if node.addon ~= "PFQUEST" then
|
|
return
|
|
end
|
|
if not pfQuest.questlog or not pfQuest.questlog[questid] then
|
|
return
|
|
end
|
|
elseif tracker.mode == "GIVER_TRACKING" then -- skip everything that isn't a questgiver
|
|
if node.addon ~= "PFQUEST" then
|
|
return
|
|
end
|
|
-- break on already taken quests
|
|
if not pfQuest.questlog or pfQuest.questlog[questid] then
|
|
return
|
|
end
|
|
-- every layer above 2 is not a questgiver
|
|
if not node.layer or node.layer > 2 then
|
|
return
|
|
end
|
|
elseif tracker.mode == "DATABASE_TRACKING" then -- skip everything that isn't db query
|
|
if node.addon ~= "PFDB" then
|
|
return
|
|
end
|
|
end
|
|
|
|
local id
|
|
|
|
-- O(1) duplicate check via reverse map (replaces linear scan of tracker.buttons)
|
|
local existing = tracker.buttonByTitle[TrackerTitleKey(title)]
|
|
if existing then
|
|
local button = tracker.buttons[existing]
|
|
if node.dummy or not node.texture then
|
|
id = existing -- node icon takes slot
|
|
elseif node.cluster and (not button.node or button.node.texture) then
|
|
id = existing -- cluster icon, acceptable
|
|
else
|
|
return -- no icon update needed
|
|
end
|
|
end
|
|
|
|
if not id then
|
|
-- use maxcount + 1 as default id
|
|
id = table.getn(tracker.buttons) + 1
|
|
|
|
-- detect a reusable button
|
|
for bid, button in pairs(tracker.buttons) do
|
|
if button.empty then
|
|
id = bid
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if id > 25 then
|
|
return
|
|
end
|
|
|
|
-- create one if required
|
|
if not tracker.buttons[id] then
|
|
tracker.buttons[id] = CreateFrame("Button", "pfQuestMapButton" .. id, tracker)
|
|
tracker.buttons[id]:SetHeight(entryheight)
|
|
|
|
tracker.buttons[id].bg = tracker.buttons[id]:CreateTexture(nil, "BACKGROUND")
|
|
tracker.buttons[id].bg:SetTexture(1, 1, 1, 0.2)
|
|
tracker.buttons[id].bg:SetAllPoints()
|
|
tracker.buttons[id].bg:SetAlpha(0)
|
|
|
|
tracker.buttons[id].text = tracker.buttons[id]:CreateFontString("pfQuestIDButton", "HIGH", "GameFontNormal")
|
|
tracker.buttons[id].text:SetFont(pfUI.font_default, fontsize)
|
|
tracker.buttons[id].text:SetJustifyH("LEFT")
|
|
tracker.buttons[id].text:SetPoint("TOPLEFT", 16, -4)
|
|
tracker.buttons[id].text:SetPoint("TOPRIGHT", -10, -4)
|
|
|
|
tracker.buttons[id].icon = tracker.buttons[id]:CreateTexture(nil, "BORDER")
|
|
tracker.buttons[id].icon:SetPoint("TOPLEFT", 2, -4)
|
|
tracker.buttons[id].icon:SetWidth(12)
|
|
tracker.buttons[id].icon:SetHeight(12)
|
|
|
|
tracker.buttons[id]:RegisterEvent("QUEST_WATCH_UPDATE")
|
|
tracker.buttons[id]:RegisterEvent("QUEST_LOG_UPDATE")
|
|
tracker.buttons[id]:RegisterEvent("QUEST_FINISHED")
|
|
|
|
tracker.buttons[id]:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
|
|
|
tracker.buttons[id]:SetScript("OnEnter", tracker.ButtonEnter)
|
|
tracker.buttons[id]:SetScript("OnLeave", tracker.ButtonLeave)
|
|
tracker.buttons[id]:SetScript("OnEvent", function() this.contentPending = true end)
|
|
tracker.buttons[id]:SetScript("OnClick", tracker.ButtonClick)
|
|
end
|
|
|
|
-- set required data
|
|
tracker.buttons[id].empty = nil
|
|
tracker.buttons[id].title = title
|
|
tracker.buttons[id].node = node
|
|
tracker.buttons[id].questid = questid
|
|
|
|
-- keep reverse map in sync
|
|
tracker.buttonByTitle[TrackerTitleKey(title)] = id
|
|
|
|
-- reload button data
|
|
tracker.ButtonEvent(tracker.buttons[id])
|
|
tracker.ButtonUpdate(tracker.buttons[id])
|
|
end
|
|
|
|
function tracker.Reset()
|
|
tracker:SetHeight(panelheight)
|
|
tracker.questPoints = {}
|
|
for id, button in pairs(tracker.buttons) do
|
|
button.level = nil
|
|
button.title = nil
|
|
button.perc = nil
|
|
button.empty = true
|
|
button:SetHeight(0)
|
|
button:Hide()
|
|
end
|
|
-- reverse map is only valid while buttons hold titles; clear on full reset
|
|
for k in pairs(tracker.buttonByTitle) do
|
|
tracker.buttonByTitle[k] = nil
|
|
end
|
|
|
|
-- add tracked quests
|
|
local _, numQuests = GetNumQuestLogEntries()
|
|
local found = 0
|
|
|
|
-- iterate over all quests
|
|
for qlogid = 1, 40 do
|
|
local title, level, tag, header, collapsed, complete = compat.GetQuestLogTitle(qlogid)
|
|
if title and not header then
|
|
local watched = IsQuestWatched(qlogid)
|
|
-- "All Quests" must not depend on the client marking a quest as watched.
|
|
-- Turtle leaves some normal item/object quests (for example Hilary's
|
|
-- Necklace) unwatched, which previously made them disappear after the
|
|
-- tracker reset even though they were active in the quest log.
|
|
local trackingmethod = tonumber(pfQuest_config["trackingmethod"])
|
|
if trackingmethod ~= 5 and (watched or trackingmethod == 1) then
|
|
-- Objective rows can briefly be absent while the client reindexes the
|
|
-- quest log after a turn-in. Use the authoritative completion flag.
|
|
local img = complete
|
|
and pfQuestConfig.path .. "\\img\\complete_c"
|
|
or pfQuestConfig.path .. "\\img\\complete"
|
|
pfQuest.tracker.ButtonAdd(title, { dummy = true, addon = "PFQUEST", texture = img })
|
|
end
|
|
|
|
found = found + 1
|
|
if found >= numQuests then
|
|
break
|
|
end
|
|
end
|
|
end
|
|
-- Note: DoLayout is called by UpdateNodes after all ButtonAdd calls complete
|
|
end
|
|
|
|
-- make global available
|
|
pfQuest.tracker = tracker
|