Add OctoQuestRewards 1.0.0 addon source
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
local A = OctoQuestRewards
|
||||
local G = getfenv(0)
|
||||
A.predictions = {}
|
||||
A.frame = CreateFrame("Frame", "OctoQuestRewardsEvents")
|
||||
|
||||
function A.Print(text)
|
||||
if DEFAULT_CHAT_FRAME then DEFAULT_CHAT_FRAME:AddMessage("|cff88ccffOQR:|r " .. text) end
|
||||
end
|
||||
|
||||
function A.DebugRecord(r, force)
|
||||
local text = "ID=" .. tostring(r.id or "?") .. " title=" .. r.title
|
||||
.. " questLevel=" .. tostring(r.level or "?") .. " playerLevel=" .. r.playerLevel
|
||||
.. " IDsource=" .. r.idSource .. " XPsource=" .. r.xpSource
|
||||
.. " predictedXP=" .. tostring(r.xp or "unknown")
|
||||
.. " predictedMoney=" .. tostring(r.money or "unknown") .. "c"
|
||||
.. " moneySource=" .. tostring(r.moneySource or "unavailable")
|
||||
if force or text ~= A.lastDebug then A.Print(text); A.lastDebug = text end
|
||||
end
|
||||
|
||||
-- One deferred update per event burst, removed immediately. No idle polling.
|
||||
function A.Schedule()
|
||||
A.frame:SetScript("OnUpdate", function()
|
||||
A.frame:SetScript("OnUpdate", nil)
|
||||
A.RenderAll()
|
||||
end)
|
||||
end
|
||||
|
||||
local function Difference(actual, predicted)
|
||||
if actual == nil or predicted == nil then return "unknown" end
|
||||
return tostring(actual - predicted)
|
||||
end
|
||||
|
||||
function A.Report(r, xp, money, source)
|
||||
if not A.db.debug then return end
|
||||
if r then A.DebugRecord(r, true) end
|
||||
A.Print("actualXP=" .. tostring(xp or "unknown") .. " actualMoney=" .. tostring(money or "unknown")
|
||||
.. "c differenceXP(actual-predicted)=" .. Difference(xp, r and r.xp)
|
||||
.. " differenceMoney=" .. Difference(money, r and r.money) .. "c source=" .. source)
|
||||
A.db.lastTurnIn = { id = r and r.id, title = r and r.title,
|
||||
playerLevel = r and r.playerLevel, predictedXP = r and r.xp,
|
||||
predictedMoney = r and r.money, xp = xp, money = money, source = source }
|
||||
end
|
||||
|
||||
function A.BeginTurnIn()
|
||||
if not A.db.debug then return end
|
||||
local r = A.GetRecord("reward")
|
||||
if r then
|
||||
A.pending = { record = r, expires = GetTime() + 8 }
|
||||
A.DebugRecord(r, true)
|
||||
end
|
||||
end
|
||||
|
||||
local function EscapePattern(text)
|
||||
return (string.gsub(text, "([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1"))
|
||||
end
|
||||
|
||||
local function CaptureFormat(message, template, marker, capture)
|
||||
if not template then return nil end
|
||||
local pattern = EscapePattern(template)
|
||||
pattern = string.gsub(pattern, EscapePattern(EscapePattern(marker)), function() return capture end)
|
||||
local _, _, value = string.find(message, "^" .. pattern .. "$")
|
||||
return value
|
||||
end
|
||||
|
||||
local function ReadChatMoney(text)
|
||||
local value, found = 0, false
|
||||
for _, unit in ipairs({{G.GOLD, 10000}, {G.SILVER, 100}, {G.COPPER, 1}}) do
|
||||
if type(unit[1]) == "string" then
|
||||
local _, _, amount = string.find(string.lower(text), "(%d+)%s*" .. EscapePattern(string.lower(unit[1])))
|
||||
if amount then value, found = value + tonumber(amount) * unit[2], true end
|
||||
end
|
||||
end
|
||||
if found then return value end
|
||||
end
|
||||
|
||||
function A.SystemMessage(message)
|
||||
local p = A.pending
|
||||
if not A.db.debug or not p or type(message) ~= "string" then return end
|
||||
if GetTime() > p.expires then A.pending = nil; return end
|
||||
local title = CaptureFormat(message, G.ERR_QUEST_COMPLETE_S, "%s", "(.-)")
|
||||
if title then
|
||||
if title ~= p.record.title then A.pending = nil; return end
|
||||
p.confirmed = true
|
||||
end
|
||||
local xp = CaptureFormat(message, G.ERR_QUEST_REWARD_EXP_I, "%d", "(%d+)")
|
||||
if xp then p.xp = tonumber(xp) end
|
||||
local moneyText = CaptureFormat(message, G.ERR_QUEST_REWARD_MONEY_S, "%s", "(.-)")
|
||||
if moneyText then p.money = ReadChatMoney(moneyText) end
|
||||
if p.confirmed and (title or xp or moneyText) then
|
||||
A.Report(p.record, p.xp, p.money, "server quest system messages; missing values remain unknown")
|
||||
end
|
||||
end
|
||||
|
||||
-- This Vanilla function has one argument and no return payload to preserve.
|
||||
-- Wrapping is necessary for correlating server chat with an actual submission.
|
||||
local function SetupHooks()
|
||||
if type(G.GetQuestReward) == "function" then
|
||||
local old = G.GetQuestReward
|
||||
G.GetQuestReward = function(choice)
|
||||
A.BeginTurnIn()
|
||||
return old(choice)
|
||||
end
|
||||
end
|
||||
if type(G.QuestLog_UpdateQuestDetails) == "function" then
|
||||
if type(G.hooksecurefunc) == "function" then
|
||||
G.hooksecurefunc("QuestLog_UpdateQuestDetails", A.Schedule)
|
||||
else
|
||||
local old = G.QuestLog_UpdateQuestDetails
|
||||
-- Inspected Vanilla FrameXML: single doNotScroll argument, no returns.
|
||||
G.QuestLog_UpdateQuestDetails = function(doNotScroll)
|
||||
old(doNotScroll)
|
||||
A.Schedule()
|
||||
end
|
||||
end
|
||||
end
|
||||
A.SetupUI()
|
||||
end
|
||||
|
||||
local function Has(fn) return type(fn) == "function" and "yes" or "no" end
|
||||
function A.Status()
|
||||
local version, build = GetBuildInfo()
|
||||
A.Print("1.0.0 loaded; client=" .. tostring(version) .. " build=" .. tostring(build)
|
||||
.. " content=" .. tostring(G.TURTLE_WOW_VERSION or "unknown") .. "; debug=" .. tostring(A.db.debug)
|
||||
.. "; XP scaling=" .. A.db.scaling .. " (unverified server match, assumed 1x)")
|
||||
A.Print("NPC money API=" .. Has(G.GetRewardMoney) .. "; log money API=" .. Has(G.GetQuestLogRewardMoney)
|
||||
.. "; ClassicAPI quest cache=" .. Has(C_QuestLog and C_QuestLog.GetQuestDetails)
|
||||
.. "; pfQuest=" .. tostring(pfDB ~= nil) .. "; pfExtend XP=" .. tostring(PfExtend_QuestRewards ~= nil)
|
||||
.. "; QUEST_TURNED_IN=" .. (A.turnedInSeen and "observed" or "not observed")
|
||||
.. "; event registration=" .. tostring(A.turnInRegistered))
|
||||
if A.current then A.DebugRecord(A.current, true) end
|
||||
local last = A.db.lastTurnIn
|
||||
if last then
|
||||
A.Print("last receipt: ID=" .. tostring(last.id or "?") .. " title=" .. tostring(last.title or "?")
|
||||
.. " actualXP=" .. tostring(last.xp or "unknown") .. " actualMoney=" .. tostring(last.money or "unknown")
|
||||
.. "c source=" .. tostring(last.source))
|
||||
end
|
||||
end
|
||||
|
||||
local function Command(message)
|
||||
local cmd = string.lower(message or "")
|
||||
if cmd == "debug" then
|
||||
A.db.debug = not A.db.debug
|
||||
A.pending, A.lastDebug = nil, nil
|
||||
A.Print("Debug " .. (A.db.debug and "enabled" or "disabled") .. ".")
|
||||
A.Schedule()
|
||||
elseif cmd == "status" then A.Status()
|
||||
elseif cmd == "scaling vanilla" or cmd == "scaling full25" then
|
||||
A.db.scaling = cmd == "scaling vanilla" and "vanilla" or "full25"
|
||||
A.Print("XP scaling assumption: " .. A.db.scaling .. "; estimates remain unverified.")
|
||||
A.Schedule()
|
||||
else
|
||||
A.Print("/oqr debug toggles turn-in comparisons; /oqr status shows sources and last receipt.")
|
||||
A.Print("Optional: /oqr scaling vanilla or /oqr scaling full25. Default: Vanilla scaling, 1x rate.")
|
||||
end
|
||||
end
|
||||
|
||||
local initialized
|
||||
A.frame:SetScript("OnEvent", function()
|
||||
if event == "PLAYER_LOGIN" then
|
||||
if initialized then return end
|
||||
initialized = true
|
||||
OctoQuestRewardsDB = OctoQuestRewardsDB or {}
|
||||
A.db = OctoQuestRewardsDB
|
||||
if A.db.scaling ~= "full25" then A.db.scaling = "vanilla" end
|
||||
SLASH_OCTOQUESTREWARDS1 = "/oqr"
|
||||
SlashCmdList.OCTOQUESTREWARDS = Command
|
||||
SetupHooks()
|
||||
A.turnInRegistered = pcall(A.frame.RegisterEvent, A.frame, "QUEST_TURNED_IN")
|
||||
if C_QuestLog then pcall(A.frame.RegisterEvent, A.frame, "QUEST_DATA_LOAD_RESULT") end
|
||||
A.Schedule()
|
||||
elseif not initialized then return
|
||||
elseif event == "QUEST_TURNED_IN" then
|
||||
A.turnedInSeen = true
|
||||
local id, xp, money = tonumber(arg1), tonumber(arg2), tonumber(arg3)
|
||||
local r = id and A.predictions[id]
|
||||
if A.pending and (not r or r.id == A.pending.record.id) then r = A.pending.record end
|
||||
if not r then
|
||||
r = { id = id, title = "unknown", playerLevel = UnitLevel("player"),
|
||||
idSource = "QUEST_TURNED_IN", xpSource = "unavailable" }
|
||||
end
|
||||
A.Report(r, xp, money, "QUEST_TURNED_IN server completion packet")
|
||||
A.pending = nil
|
||||
elseif event == "CHAT_MSG_SYSTEM" then A.SystemMessage(arg1)
|
||||
elseif event == "QUEST_FINISHED" or event == "QUEST_GREETING" then
|
||||
A.current = nil
|
||||
A.Schedule()
|
||||
else A.Schedule() end
|
||||
end)
|
||||
|
||||
for _, name in ipairs({"PLAYER_LOGIN", "QUEST_DETAIL", "QUEST_PROGRESS", "QUEST_COMPLETE",
|
||||
"QUEST_FINISHED", "QUEST_GREETING", "QUEST_ITEM_UPDATE", "QUEST_LOG_UPDATE", "PLAYER_LEVEL_UP",
|
||||
"CHAT_MSG_SYSTEM", "UI_SCALE_CHANGED"}) do
|
||||
pcall(A.frame.RegisterEvent, A.frame, name)
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
## Interface: 11200
|
||||
## Title: OctoQuestRewards
|
||||
## Notes: Quest XP estimates in the original quest UI, with optional reward debugging.
|
||||
## Author: John The Baptist
|
||||
## Version: 1.0.0
|
||||
## OptionalDeps: pfQuest, pfQuest-turtle, pfExtend, ClassicAPI, AI_VoiceOver
|
||||
## SavedVariablesPerCharacter: OctoQuestRewardsDB
|
||||
|
||||
QuestData.lua
|
||||
VanillaXP.lua
|
||||
UI.lua
|
||||
Core.lua
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
OctoQuestRewards = {}
|
||||
local A = OctoQuestRewards
|
||||
local G = getfenv(0)
|
||||
-- A raw custom base XP cross-check, not the website's potentially scaled Gains.
|
||||
-- https://octowow.st/db/?quest=41677 (RewXP=2150, level=32, checked 2026-10-05)
|
||||
local octoBase = { [41677] = {32, 2150} }
|
||||
|
||||
-- Only documented, present functions are called. No modern XP API is assumed.
|
||||
function A.Call(fn, a)
|
||||
if type(fn) ~= "function" then return nil end
|
||||
local ok, value
|
||||
if a == nil then ok, value = pcall(fn) else ok, value = pcall(fn, a) end
|
||||
if ok then return value end
|
||||
end
|
||||
|
||||
local function QuestTables()
|
||||
local q = pfDB and pfDB.quests
|
||||
return q and q.data, q and (q.loc or q[GetLocale()] or q.enUS)
|
||||
end
|
||||
|
||||
local titleIndex, indexedLocale
|
||||
local function MatchTitle(title, level, description, objectives)
|
||||
local data, loc = QuestTables()
|
||||
if not data or not loc or not title then return nil end
|
||||
if indexedLocale ~= loc then
|
||||
titleIndex, indexedLocale = {}, loc
|
||||
for id, row in pairs(loc) do
|
||||
if type(row) == "table" and row.T and data[id] then
|
||||
titleIndex[row.T] = titleIndex[row.T] or {}
|
||||
table.insert(titleIndex[row.T], id)
|
||||
end
|
||||
end
|
||||
end
|
||||
local found, count = nil, 0
|
||||
for _, id in ipairs(titleIndex[title] or {}) do
|
||||
local row = data[id]
|
||||
if not level or level == row.lvl or row.lvl == -1 then
|
||||
found, count = id, count + 1
|
||||
end
|
||||
end
|
||||
if count == 1 then return found, "pfQuest title/level match (inferred ID)" end
|
||||
-- Duplicate titles need exact quest text; never pick the first guessed ID.
|
||||
found, count = nil, 0
|
||||
if pfDatabase and pfDatabase.FormatQuestText then
|
||||
for _, id in ipairs(titleIndex[title] or {}) do
|
||||
local row = loc[id]
|
||||
local d = row.D and pfDatabase:FormatQuestText(row.D)
|
||||
local o = row.O and pfDatabase:FormatQuestText(row.O)
|
||||
if (not level or level == data[id].lvl or data[id].lvl == -1)
|
||||
and ((description and description ~= "" and description == d)
|
||||
or (objectives and objectives ~= "" and objectives == o)) then
|
||||
found, count = id, count + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
if count == 1 then return found, "pfQuest exact text match (inferred ID)" end
|
||||
end
|
||||
|
||||
local function PositiveID(id)
|
||||
id = tonumber(id)
|
||||
if id and id > 0 then return id end
|
||||
end
|
||||
|
||||
function A.LogTitle(index)
|
||||
if pfQuestCompat and pfQuestCompat.GetQuestLogTitle then
|
||||
return pfQuestCompat.GetQuestLogTitle(index)
|
||||
end
|
||||
return GetQuestLogTitle(index)
|
||||
end
|
||||
|
||||
function A.LogID(index, title, level, description, objectives)
|
||||
if C_QuestLog and C_QuestLog.GetQuestIDForLogIndex then
|
||||
local id = PositiveID(A.Call(C_QuestLog.GetQuestIDForLogIndex, index))
|
||||
if id then return id, "C_QuestLog.GetQuestIDForLogIndex" end
|
||||
end
|
||||
local link = A.Call(G.GetQuestLink, index)
|
||||
if type(link) == "string" then
|
||||
local _, _, id = string.find(link, "quest:(%d+)")
|
||||
if PositiveID(id) then return tonumber(id), "GetQuestLink" end
|
||||
end
|
||||
return MatchTitle(title, level, description, objectives)
|
||||
end
|
||||
|
||||
-- Vanilla degradation. Octo's server rate/scaling is not verified.
|
||||
-- Level alone cannot determine base XP: quests at the same level vary widely.
|
||||
function A.EstimateXP(base, questLevel, playerLevel)
|
||||
if type(base) ~= "number" or not questLevel then return nil end
|
||||
if playerLevel >= 60 then return 0 end
|
||||
if questLevel < 1 then questLevel = playerLevel end
|
||||
local difference = playerLevel - questLevel
|
||||
local factor = 1
|
||||
if A.db.scaling == "full25" then
|
||||
-- Optional explicit assumption for Turtle-like custom scaling.
|
||||
if difference > 25 then return nil end
|
||||
elseif difference >= 10 then factor = 0.1
|
||||
elseif difference > 5 then factor = (10 - difference) * 0.2 end
|
||||
return math.ceil(base * factor - 0.000001)
|
||||
end
|
||||
|
||||
function A.GetRecord(kind)
|
||||
local r = { playerLevel = UnitLevel("player"), kind = kind }
|
||||
local index
|
||||
if kind == "log" then
|
||||
index = GetQuestLogSelection()
|
||||
local header
|
||||
local tag
|
||||
r.title, r.level, tag, header = A.LogTitle(index)
|
||||
if not r.title or header then return nil end
|
||||
local description, objectives = GetQuestLogQuestText()
|
||||
r.id, r.idSource = A.LogID(index, r.title, r.level, description, objectives)
|
||||
r.money = A.Call(G.GetQuestLogRewardMoney)
|
||||
r.moneySource = "GetQuestLogRewardMoney (server cache)"
|
||||
else
|
||||
r.title = GetTitleText()
|
||||
if not r.title or r.title == "" then return nil end
|
||||
-- VoiceOver installs GetQuestID inside its own namespace, not the engine.
|
||||
r.id = PositiveID(A.Call(G.GetQuestID))
|
||||
if r.id then r.idSource = "GetQuestID (provider unverified)" end
|
||||
if not r.id then
|
||||
r.id, r.idSource = MatchTitle(r.title, nil,
|
||||
kind == "detail" and GetQuestText() or nil,
|
||||
kind == "detail" and GetObjectiveText() or nil)
|
||||
end
|
||||
if kind == "progress" then
|
||||
-- REQUEST_ITEMS packets don't supply reward money: GetRewardMoney here
|
||||
-- can be stale. Read only a uniquely matching accepted quest instead.
|
||||
local match, count = nil, 0
|
||||
for i = 1, GetNumQuestLogEntries() do
|
||||
local title, level, tag, header = A.LogTitle(i)
|
||||
if title == r.title and not header then match, count = i, count + 1 end
|
||||
end
|
||||
if count == 1 then
|
||||
local old = GetQuestLogSelection()
|
||||
SelectQuestLogEntry(match)
|
||||
local log = A.GetRecord("log")
|
||||
SelectQuestLogEntry(old)
|
||||
if log then
|
||||
r.id, r.idSource, r.level = log.id, log.idSource, log.level
|
||||
r.money, r.moneySource = log.money, log.moneySource
|
||||
end
|
||||
end
|
||||
else
|
||||
r.money = A.Call(G.GetRewardMoney)
|
||||
r.moneySource = "GetRewardMoney (NPC server packet)"
|
||||
end
|
||||
end
|
||||
local data = QuestTables()
|
||||
if not r.level and data and r.id and data[r.id] then r.level = data[r.id].lvl end
|
||||
local cache
|
||||
if r.id and C_QuestLog and C_QuestLog.GetQuestDetails then
|
||||
cache = A.Call(C_QuestLog.GetQuestDetails, r.id)
|
||||
if type(cache) == "table" then
|
||||
r.level = cache.level or r.level
|
||||
if r.money == nil and r.playerLevel < 60 then
|
||||
r.money, r.moneySource = cache.rewardMoney, "C_QuestLog.GetQuestDetails.rewardMoney"
|
||||
end
|
||||
end
|
||||
end
|
||||
if r.money and r.money < 0 then r.money = 0 end -- A cost isn't a reward.
|
||||
local reward = r.id and PfExtend_QuestRewards and PfExtend_QuestRewards[r.id]
|
||||
local baseline = r.id and (octoBase[r.id] or (A.VanillaXP and A.VanillaXP[r.id]))
|
||||
local base
|
||||
if baseline and (baseline[1] == r.level or baseline[1] == -1) then
|
||||
base = baseline[2]
|
||||
r.xpSource = (octoBase[r.id] and "Octo DB raw RewXP" or "LibQuestXP Vanilla baseline")
|
||||
.. " + " .. A.db.scaling .. " scaling; rate assumed 1x; Octo match unverified"
|
||||
elseif not reward and type(cache) == "table" and type(cache.rewardMoneyAtMaxLevel) == "number" then
|
||||
-- Classic cache has no XP field. This is the historical Vanilla conversion,
|
||||
-- NOT a directly supplied XP value, and may differ on custom quests.
|
||||
if r.level and r.level <= 60 then
|
||||
base = cache.rewardMoneyAtMaxLevel / 0.6
|
||||
r.xpSource = "ClassicAPI max-level money / 0.6 + " .. A.db.scaling .. " scaling (estimated)"
|
||||
end
|
||||
end
|
||||
r.baseXP = base
|
||||
r.xp = A.EstimateXP(base, r.level, r.playerLevel)
|
||||
if base == nil and reward and type(reward.xp) == "number" then
|
||||
-- The installed pfExtend scraper copies displayed Gains, not raw RewXP.
|
||||
-- We cannot undo unknown website scaling. Use it only as a near-level
|
||||
-- reference, with no second scaling; suppress it on overlevelled quests.
|
||||
r.xpSource = "pfExtend website Gains reference; base XP/scaling unverified"
|
||||
if r.playerLevel >= 60 then r.xp = 0
|
||||
elseif r.level and (r.level < 1 or r.playerLevel <= r.level + 5) then r.xp = reward.xp end
|
||||
end
|
||||
r.idSource = r.idSource or "unresolved/ambiguous ID"
|
||||
r.xpSource = r.xpSource or "unavailable"
|
||||
if r.id then A.predictions[r.id] = r end
|
||||
return r
|
||||
end
|
||||
@@ -0,0 +1,146 @@
|
||||
local A = OctoQuestRewards
|
||||
local G = getfenv(0)
|
||||
local panels = {
|
||||
detail = { "QuestFrameDetailPanel", "QuestDetailScrollChildFrame", "QuestDetailScrollFrame", "QuestDescription", "QuestDetailRewardTitleText" },
|
||||
progress = { "QuestFrameProgressPanel", "QuestProgressScrollChildFrame", "QuestProgressScrollFrame", "QuestProgressText" },
|
||||
reward = { "QuestFrameRewardPanel", "QuestRewardScrollChildFrame", "QuestRewardScrollFrame", "QuestRewardText", "QuestRewardRewardTitleText" },
|
||||
log = { "QuestLogFrame", "QuestLogDetailScrollChildFrame", "QuestLogDetailScrollFrame", "QuestLogQuestDescription", "QuestLogRewardTitleText" },
|
||||
}
|
||||
|
||||
-- Copy the live font, including addon overrides, rather than just its template.
|
||||
local function CopyFont(target, source)
|
||||
if not source or not source.GetFont then return end
|
||||
local font, size, flags = source:GetFont()
|
||||
if font and size then target:SetFont(font, size, flags or "") end
|
||||
end
|
||||
|
||||
function A.Number(value)
|
||||
local s = tostring(math.floor(value))
|
||||
local count
|
||||
repeat s, count = string.gsub(s, "^(%-?%d+)(%d%d%d)", "%1,%2") until count == 0
|
||||
return s
|
||||
end
|
||||
|
||||
function A.Money(copper)
|
||||
copper = math.floor(copper)
|
||||
local gold = math.floor(copper / 10000)
|
||||
local silver = math.floor(copper / 100) - gold * 100
|
||||
local coins = copper - math.floor(copper / 100) * 100
|
||||
local parts = {}
|
||||
if gold > 0 then table.insert(parts, gold .. "g") end
|
||||
if silver > 0 then table.insert(parts, silver .. "s") end
|
||||
if coins > 0 then table.insert(parts, coins .. "c") end
|
||||
return table.concat(parts, " ")
|
||||
end
|
||||
|
||||
-- Read actual visible content extents, including the nested fading reward area.
|
||||
-- Ignore textures and spacer frames, whose bounds aren't content bounds.
|
||||
local function ContentBottom(object, block, top, scale, depth)
|
||||
if object == block or not object:IsShown() or depth > 5 then return 0 end
|
||||
local name = object:GetName() or ""
|
||||
if string.find(name, "Spacer") then return 0 end
|
||||
local bottom = 0
|
||||
local isText = object:IsObjectType("FontString")
|
||||
if (isText and object:GetText() and object:GetText() ~= "")
|
||||
or object:IsObjectType("Button") or string.find(name, "MoneyFrame$") then
|
||||
local b = object:GetBottom()
|
||||
if b then
|
||||
-- FontStrings inherit their parent's scale; don't assume Frame-only
|
||||
-- methods exist on Vanilla regions.
|
||||
local owner = isText and object:GetParent() or object
|
||||
bottom = (top * scale - b * owner:GetEffectiveScale()) / scale
|
||||
end
|
||||
end
|
||||
if object.GetRegions then
|
||||
for _, region in ipairs({object:GetRegions()}) do
|
||||
bottom = math.max(bottom, ContentBottom(region, block, top, scale, depth + 1))
|
||||
end
|
||||
end
|
||||
if object.GetChildren then
|
||||
for _, child in ipairs({object:GetChildren()}) do
|
||||
bottom = math.max(bottom, ContentBottom(child, block, top, scale, depth + 1))
|
||||
end
|
||||
end
|
||||
return bottom
|
||||
end
|
||||
|
||||
function A.Render(kind)
|
||||
local cfg = panels[kind]
|
||||
local panel, child, scroll = G[cfg[1]], G[cfg[2]], G[cfg[3]]
|
||||
if not panel or not child or not scroll then return end
|
||||
if cfg.block then cfg.block:Hide() end
|
||||
if not panel:IsVisible() then return end
|
||||
local r = A.GetRecord(kind)
|
||||
A.current = r
|
||||
if A.db.debug and r then A.DebugRecord(r) end
|
||||
local lines = {}
|
||||
if r and r.xp and r.xp > 0 then table.insert(lines, "Estimated XP: " .. A.Number(r.xp)) end
|
||||
if not cfg.block then
|
||||
cfg.block = CreateFrame("Frame", nil, child)
|
||||
cfg.block:SetWidth(child:GetWidth() - 15)
|
||||
cfg.block.title = cfg.block:CreateFontString(nil, "ARTWORK", "QuestTitleFont")
|
||||
cfg.block.title:SetPoint("TOPLEFT", cfg.block, "TOPLEFT", 0, 0)
|
||||
cfg.block.title:SetText("Rewards")
|
||||
cfg.block.text = cfg.block:CreateFontString(nil, "ARTWORK", "QuestFont")
|
||||
cfg.block.text:SetJustifyH("LEFT")
|
||||
cfg.baseHeight = child:GetHeight()
|
||||
end
|
||||
local block = cfg.block
|
||||
-- Keep our previous extension out of the next layout calculation.
|
||||
if cfg.lastHeight and child:GetHeight() == cfg.lastHeight then child:SetHeight(cfg.baseHeight) end
|
||||
if table.getn(lines) == 0 then
|
||||
block:Hide()
|
||||
scroll:UpdateScrollChildRect()
|
||||
return
|
||||
end
|
||||
local top, scale = child:GetTop(), child:GetEffectiveScale()
|
||||
if not top then block:Hide(); return end
|
||||
local bottom = ContentBottom(child, block, top, scale, 0)
|
||||
local heading = cfg[5] and G[cfg[5]]
|
||||
local example = G[cfg[4]]
|
||||
CopyFont(block.text, example)
|
||||
CopyFont(block.title, heading or G.QuestProgressTitleText)
|
||||
local titleHeight = 0
|
||||
if heading and heading:IsShown() then block.title:Hide()
|
||||
else
|
||||
block.title:Show()
|
||||
titleHeight = block.title:GetHeight() + 5
|
||||
end
|
||||
block:ClearAllPoints()
|
||||
block:SetPoint("TOPLEFT", child, "TOPLEFT", 5, -bottom - 12)
|
||||
block:SetWidth(child:GetWidth() - 15)
|
||||
block.text:ClearAllPoints()
|
||||
block.text:SetPoint("TOPLEFT", block, "TOPLEFT", 0, -titleHeight)
|
||||
block.text:SetWidth(block:GetWidth())
|
||||
block.text:SetText(table.concat(lines, "\n"))
|
||||
if example and example.GetTextColor then
|
||||
block.text:SetTextColor(example:GetTextColor())
|
||||
else block.text:SetTextColor(0, 0, 0) end
|
||||
if heading and heading.GetTextColor then block.title:SetTextColor(heading:GetTextColor()) end
|
||||
block:SetHeight(titleHeight + block.text:GetHeight())
|
||||
cfg.lastHeight = math.max(cfg.baseHeight, bottom + 12 + block:GetHeight() + 12)
|
||||
child:SetHeight(cfg.lastHeight)
|
||||
block:Show()
|
||||
scroll:UpdateScrollChildRect()
|
||||
end
|
||||
|
||||
function A.RenderAll()
|
||||
for kind in pairs(panels) do A.Render(kind) end
|
||||
end
|
||||
|
||||
function A.HookScript(frame, script, callback)
|
||||
if not frame then return end
|
||||
if frame.HookScript then frame:HookScript(script, callback); return end
|
||||
-- Vanilla has no HookScript. Chain the existing script with its original args.
|
||||
local old = frame:GetScript(script)
|
||||
frame:SetScript(script, function(...)
|
||||
if old then old(unpack(arg)) end
|
||||
callback()
|
||||
end)
|
||||
end
|
||||
|
||||
function A.SetupUI()
|
||||
for _, cfg in pairs(panels) do
|
||||
A.HookScript(G[cfg[1]], "OnShow", A.Schedule)
|
||||
end
|
||||
end
|
||||
+3541
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user