local SI, L = unpack((select(2, ...)))
local Module = SI:NewModule('Progress', 'AceEvent-3.0')
local Tooltip = SI:GetModule('Tooltip')
---@class SingleQuestEntry
---@field type "single"
---@field expansion number?
---@field index number
---@field name string
---@field questID number
---@field reset "none" | "daily" | "weekly"
---@field persists boolean
---@field fullObjective boolean
---@class AnyQuestEntry
---@field type "any"
---@field expansion number?
---@field index number
---@field name string
---@field questID number[]
---@field reset "none" | "daily" | "weekly"
---@field persists boolean
---@field fullObjective boolean
---@class QuestListEntry
---@field type "list"
---@field expansion number?
---@field index number
---@field name string
---@field questID number[]
---@field unlockQuest number?
---@field reset "none" | "daily" | "weekly"
---@field persists boolean
---@field threshold number?
---@field questAbbr table<number, string>?
---@field progress boolean
---@field onlyOnOrCompleted boolean
---@field questName table<number, string>?
---@field separateLines string[]?
---@class CustomEntry
---@field type "custom"
---@field expansion number?
---@field index number
---@field name string
---@field reset "none" | "daily" | "weekly"
---@field func fun(store: table, entry: CustomEntry): nil
---@field showFunc fun(store: table, entry: CustomEntry): string?
---@field resetFunc nil | fun(store: table, entry: CustomEntry): nil
---@field tooltipFunc nil | fun(store: table, entry: CustomEntry, toon: string): nil
---@field relatedQuest number[]?
---@alias ProgressEntry SingleQuestEntry | AnyQuestEntry | QuestListEntry | CustomEntry
---@class QuestStore
---@field show boolean?
---@field objectiveType string?
---@field isComplete boolean?
---@field isFinish boolean?
---@field numFulfilled number?
---@field numRequired number?
---@field leaderboardCount number?
---@field text string?
---@field [number] string?
---@class QuestListStore
---@field show boolean?
---@field [number] QuestStore?
---@type table<string, ProgressEntry>
local presets = {
-- Great Vault (Raid)
['great-vault-raid'] = {
type = 'custom',
index = 1,
name = RAIDS,
reset = 'weekly',
func = function(store, entry)
wipe(store)
if SI.playerLevel < SI.maxLevel then
store.unlocked = false
else
store.unlocked = true
local activities = C_WeeklyRewards.GetActivities(Enum.WeeklyRewardChestThresholdType.Raid)
sort(activities, entry.activityCompare)
for i, activityInfo in ipairs(activities) do
if activityInfo.progress >= activityInfo.threshold then
store[i] = activityInfo.level
end
end
local rewardWaiting = C_WeeklyRewards.HasAvailableRewards() and C_WeeklyRewards.CanClaimRewards()
store.rewardWaiting = rewardWaiting
end
end,
showFunc = function(store, entry)
if not store.unlocked then
return
end
local text
for index = 1, #store do
if store[index] then
text = (index > 1 and (text .. " / ") or "") .. (entry.difficultyNames[store[index]] or GetDifficultyInfo(store[index]))
end
end
if store.rewardWaiting then
if not text then
text = SI.questTurnin
else
text = text .. "(" .. SI.questTurnin .. ")"
end
end
return text
end,
resetFunc = function(store)
local unlocked = store.unlocked
local rewardWaiting = not not store[1]
wipe(store)
store.unlocked = unlocked
store.rewardWaiting = rewardWaiting
end,
-- addition info
activityCompare = function(left, right)
return left.index < right.index
end,
difficultyNames = {
[17] = 'L',
[14] = 'N',
[15] = 'H',
[16] = 'M',
},
},
-- Great Vault (PvP)
['great-vault-pvp'] = {
type = 'custom',
index = 2,
name = PVP,
reset = 'weekly',
func = function(store)
wipe(store)
if SI.playerLevel < SI.maxLevel then
store.unlocked = false
store.isComplete = false
else
local weeklyProgress = C_WeeklyRewards.GetConquestWeeklyProgress()
local rewardWaiting = C_WeeklyRewards.HasAvailableRewards() and C_WeeklyRewards.CanClaimRewards()
store.unlocked = true
store.isComplete = weeklyProgress.progress >= weeklyProgress.maxProgress
store.numFulfilled = weeklyProgress.progress
store.numRequired = weeklyProgress.maxProgress
store.unlocksCompleted = weeklyProgress.unlocksCompleted
store.maxUnlocks = weeklyProgress.maxUnlocks
store.rewardWaiting = rewardWaiting
end
end,
showFunc = function(store)
local text
if not store.unlocked then
return
elseif store.isComplete then
text = SI.questCheckMark
else
text = store.numFulfilled .. "/" .. store.numRequired
end
if store.unlocksCompleted and store.maxUnlocks then
text = text .. "(" .. store.unlocksCompleted .. "/" .. store.maxUnlocks .. ")"
end
if store.rewardWaiting then
text = text .. "(" .. SI.questTurnin .. ")"
end
return text
end,
resetFunc = function(store)
local unlocked = store.unlocked
local numRequired = store.numRequired
local maxUnlocks = store.maxUnlocks
local rewardWaiting = store.unlocksCompleted and store.unlocksCompleted > 0
wipe(store)
store.unlocked = unlocked
store.isComplete = false
store.numFulfilled = 0
store.numRequired = numRequired
store.unlocksCompleted = 0
store.maxUnlocks = maxUnlocks
store.rewardWaiting = rewardWaiting
end,
},
-- The World Awaits
['the-world-awaits'] = {
type = 'single',
index = 3,
name = L["The World Awaits"],
questID = 72728,
reset = 'weekly',
persists = false,
fullObjective = false,
},
-- Emissary of War
['emissary-of-war'] = {
type = 'single',
index = 4,
name = L["Emissary of War"],
questID = 72722,
reset = 'weekly',
persists = false,
fullObjective = false,
},
-- Timewalking
['timewalking'] = {
type = 'any',
index = 5,
name = L["Timewalking Weekend Event"],
questID = {
72727, -- A Burning Path Through Time - TBC Timewalking
72726, -- A Frozen Path Through Time - WLK Timewalking
72810, -- A Shattered Path Through Time - CTM Timewalking
72725, -- A Shrouded Path Through Time - MOP Timewalking
72724, -- A Savage Path Through Time - WOD Timewalking
72719, -- A Fel Path Through Time - LEG Timewalking
},
reset = 'weekly',
persists = false,
fullObjective = false,
},
-- Island Expedition
['bfa-island'] = {
type = 'any',
expansion = 7,
index = 1,
name = ISLANDS_HEADER,
questID = {
53436, -- Alliance
53435, -- Horde
},
reset = 'weekly',
persists = true,
fullObjective = false,
},
-- Horrific Vision
['bfa-horrific-vision'] = {
type = 'list',
expansion = 7,
index = 2,
name = SPLASH_BATTLEFORAZEROTH_8_3_0_FEATURE1_TITLE,
questID = {
57848,
57844,
57847,
57843,
57846,
57842,
57845,
57841,
},
unlockQuest = 58634, -- Opening the Gateway
reset = 'weekly',
persists = false,
questAbbr = {
[57848] = "5 + 5",
[57844] = "5 + 4",
[57847] = "5 + 3",
[57843] = "5 + 2",
[57846] = "5 + 1",
[57842] = "5 + 0",
[57845] = "3 + 0",
[57841] = "1 + 0",
},
progress = false,
onlyOnOrCompleted = false,
questName = {
[57848] = L["Full Clear + 5 Masks"],
[57844] = L["Full Clear + 4 Masks"],
[57847] = L["Full Clear + 3 Masks"],
[57843] = L["Full Clear + 2 Masks"],
[57846] = L["Full Clear + 1 Mask"],
[57842] = L["Full Clear No Masks"],
[57845] = L["Vision Boss + 2 Bonus Objectives"],
[57841] = L["Vision Boss Only"],
},
},
-- N'Zoth Assaults
['bfa-nzoth-assault'] = {
type = 'list',
expansion = 7,
index = 3,
name = WORLD_MAP_THREATS,
questID = {
-- Uldum
57157, -- Assault: The Black Empire
55350, -- Assault: Amathet Advance
56308, -- Assault: Aqir Unearthed
-- Vale of Eternal Blossoms
56064, -- Assault: The Black Empire
57008, -- Assault: The Warring Clans
57728, -- Assault: The Endless Swarm
},
unlockQuest = 57362, -- Deeper Into the Darkness
reset = 'weekly',
persists = false,
threshold = 3,
progress = true,
onlyOnOrCompleted = true,
},
-- Lesser Visions of N'Zoth
['bfa-lesser-vision'] = {
type = 'any',
expansion = 7,
index = 4,
name = L["Lesser Visions of N'Zoth"],
questID = {
58151, -- Minions of N'Zoth
58155, -- A Hand in the Dark
58156, -- Vanquishing the Darkness
58167, -- Preventative Measures
58168, -- A Dark, Glaring Reality
},
reset = 'daily',
persists = false,
fullObjective = false,
},
-- Covenant Assaults
['sl-covenant-assault'] = {
type = 'any',
expansion = 8,
index = 1,
name = L["Covenant Assaults"],
questID = {
63823, -- Night Fae Assault
63822, -- Venthyr Assault
63824, -- Kyrian Assault
63543, -- Necrolord Assault
},
reset = 'weekly',
persists = false,
fullObjective = false,
},
-- Patterns Within Patterns
['sl-patterns-within-patterns'] = {
type = 'single',
expansion = 8,
index = 2,
name = L["Patterns Within Patterns"],
questID = 66042,
reset = 'weekly',
persists = true,
fullObjective = false,
},
-- Dragonflight Renown
['df-renown'] = {
type = 'custom',
expansion = 9,
index = 1,
name = L["Dragonflight Renown"],
reset = 'none',
func = function(store)
wipe(store)
local majorFactionIDs = C_MajorFactions.GetMajorFactionIDs(LE_EXPANSION_DRAGONFLIGHT)
for _, factionID in ipairs(majorFactionIDs) do
local data = C_MajorFactions.GetMajorFactionData(factionID)
store[factionID] = data and {data.renownLevel, data.renownReputationEarned, data.renownLevelThreshold}
end
end,
showFunc = function(store, entry)
local text
local majorFactionIDs = C_MajorFactions.GetMajorFactionIDs(LE_EXPANSION_DRAGONFLIGHT)
local factionIDs = entry.factionIDs
for _, factionID in ipairs(entry.factionIDs) do
if not text then
text = store[factionID] and store[factionID][1] or '0'
else
text = text .. ' / ' .. (store[factionID] and store[factionID][1] or '0')
end
end
for _, factionID in ipairs(majorFactionIDs) do
if not tContains(factionIDs, factionID) then
if not text then
text = store[factionID] and store[factionID][1] or '0'
else
text = text .. ' / ' .. (store[factionID] and store[factionID][1] or '0')
end
end
end
return text
end,
tooltipFunc = function(store, entry, toon)
local tip = Tooltip:AcquireIndicatorTip(2, 'LEFT', 'RIGHT')
tip:AddHeader(SI:ClassColorToon(toon), L["Dragonflight Renown"])
local majorFactionIDs = C_MajorFactions.GetMajorFactionIDs(LE_EXPANSION_DRAGONFLIGHT)
local factionIDs = entry.factionIDs
for _, factionID in ipairs(factionIDs) do
if store[factionID] then
tip:AddLine(
C_MajorFactions.GetMajorFactionData(factionID).name,
format("%s %s (%s/%s)", COVENANT_SANCTUM_TAB_RENOWN, unpack(store[factionID]))
)
else
tip:AddLine(C_MajorFactions.GetMajorFactionData(factionID).name, LOCKED)
end
end
for _, factionID in ipairs(majorFactionIDs) do
if not tContains(factionIDs, factionID) then
if store[factionID] then
tip:AddLine(
C_MajorFactions.GetMajorFactionData(factionID).name,
format("%s %s (%s/%s)", COVENANT_SANCTUM_TAB_RENOWN, unpack(store[factionID]))
)
else
tip:AddLine(C_MajorFactions.GetMajorFactionData(factionID).name, LOCKED)
end
end
end
tip:Show()
end,
-- addition info
factionIDs = {
2564, -- Loamm Niffen
2507, -- Dragonscale Expedition
2503, -- Maruuk Centaur
2511, -- Iskaara Tuskarr
2510, -- Valdrakken Accord
},
},
-- Aiding the Accord
['df-aiding-the-accord'] = {
type = 'any',
expansion = 9,
index = 2,
name = L["Aiding the Accord"],
questID = {
70750, -- Aiding the Accord
72068, -- Aiding the Accord: A Feast For All
72373, -- Aiding the Accord: The Hunt is On
72374, -- Aiding the Accord: Dragonbane Keep
72375, -- Aiding the Accord: The Isles Call
75259, -- Aiding the Accord: Zskera Vault
75859, -- Aiding the Accord: Sniffenseeking
75860, -- Aiding the Accord: Researchers Under Fire
75861, -- Aiding the Accord: Suffusion Camp
77254, -- Aiding the Accord: Time Rift
77976, -- Aiding the Accord: Dreamsurge
},
reset = 'weekly',
persists = true,
fullObjective = true,
},
-- Community Feast
['df-community-feast'] = {
type = 'single',
expansion = 9,
index = 3,
name = L["Community Feast"],
questID = 70893,
reset = 'weekly',
persists = false,
fullObjective = false,
},
-- Siege on Dragonbane Keep
['df-siege-on-dragonbane-keep'] = {
type = 'single',
expansion = 9,
index = 4,
name = L["Siege on Dragonbane Keep"],
questID = 70866,
reset = 'weekly',
persists = false,
fullObjective = false,
},
-- Grand Hunt
['df-grand-hunt'] = {
type = 'list',
expansion = 9,
index = 5,
name = L["Grand Hunt"],
questID = {
70906, -- Epic
71136, -- Rare
71137, -- Uncommon
},
reset = 'weekly',
persists = false,
progress = false,
onlyOnOrCompleted = false,
questName = {
[70906] = MAW_BUFF_QUALITY_STRING_EPIC, -- Epic
[71136] = MAW_BUFF_QUALITY_STRING_RARE, -- Rare
[71137] = MAW_BUFF_QUALITY_STRING_UNCOMMON, -- Uncommon
},
},
-- Trial of Elements
['df-trial-of-elements'] = {
type = 'single',
expansion = 9,
index = 6,
name = L["Trial of Elements"],
questID = 71995,
reset = 'weekly',
persists = false,
fullObjective = false,
},
-- Trial of Flood
['df-trial-of-flood'] = {
type = 'single',
expansion = 9,
index = 7,
name = L["Trial of Flood"],
questID = 71033,
reset = 'weekly',
persists = false,
fullObjective = false,
},
-- Primal Storms Core
['df-primal-storms-core'] = {
type = 'list',
expansion = 9,
index = 8,
name = L["Primal Storms Core"],
questID = {
73162, -- Storm's Fury
72686, -- Storm Surge
70723, -- Earth
70752, -- Water
70753, -- Air
70754, -- Fire
},
reset = 'weekly',
persists = false,
progress = false,
onlyOnOrCompleted = false,
questName = {
[73162] = L["Storm's Fury"], -- Storm's Fury
[72686] = L["Storm Surge"], -- Storm Surge
[70723] = YELLOW_FONT_COLOR_CODE .. L["Earth Core"] .. FONT_COLOR_CODE_CLOSE, -- Earth
[70752] = "|cff42a4f5" .. L["Water Core"] .. FONT_COLOR_CODE_CLOSE, -- Water
[70753] = "|cffe4f2f5" .. L["Air Core"] .. FONT_COLOR_CODE_CLOSE, -- Air
[70754] = ORANGE_FONT_COLOR_CODE .. L["Fire Core"] .. FONT_COLOR_CODE_CLOSE, -- Fire
},
},
-- Primal Storms Elementals
['df-primal-storms-elementals'] = {
type = 'list',
expansion = 9,
index = 9,
name = L["Primal Storms Elementals"],
questID = {
73991, -- Emblazion -- Fire
74005, -- Infernum
74006, -- Kain Firebrand
74016, -- Neela Firebane
73989, -- Crystalus -- Water
73993, -- Frozion
74027, -- Rouen Icewind
74009, -- Iceblade Trio
73986, -- Bouldron -- Earth
73998, -- Gravlion
73999, -- Grizzlerock
74039, -- Zurgaz Corebreaker
73995, -- Gaelzion -- Air
74007, -- Karantun
74022, -- Pipspark Thundersnap
74038, -- Voraazka
},
reset = 'daily',
persists = false,
progress = false,
onlyOnOrCompleted = false,
questName = {
[73991] = ORANGE_FONT_COLOR_CODE .. L["Emblazion"] .. FONT_COLOR_CODE_CLOSE, -- Emblazion -- Fire
[74005] = ORANGE_FONT_COLOR_CODE .. L["Infernum"] .. FONT_COLOR_CODE_CLOSE, -- Infernum
[74006] = ORANGE_FONT_COLOR_CODE .. L["Kain Firebrand"] .. FONT_COLOR_CODE_CLOSE, -- Kain Firebrand
[74016] = ORANGE_FONT_COLOR_CODE .. L["Neela Firebane"] .. FONT_COLOR_CODE_CLOSE, -- Neela Firebane
[73989] = "|cff42a4f5" .. L["Crystalus"] .. FONT_COLOR_CODE_CLOSE, -- Crystalus -- Water
[73993] = "|cff42a4f5" .. L["Frozion"] .. FONT_COLOR_CODE_CLOSE, -- Frozion
[74027] = "|cff42a4f5" .. L["Rouen Icewind"] .. FONT_COLOR_CODE_CLOSE, -- Rouen Icewind
[74009] = "|cff42a4f5" .. L["Iceblade Trio"] .. FONT_COLOR_CODE_CLOSE, -- Iceblade Trio
[73986] = YELLOW_FONT_COLOR_CODE .. L["Bouldron"] .. FONT_COLOR_CODE_CLOSE, -- Bouldron -- Earth
[73998] = YELLOW_FONT_COLOR_CODE .. L["Gravlion"] .. FONT_COLOR_CODE_CLOSE, -- Gravlion
[73999] = YELLOW_FONT_COLOR_CODE .. L["Grizzlerock"] .. FONT_COLOR_CODE_CLOSE, -- Grizzlerock
[74039] = YELLOW_FONT_COLOR_CODE .. L["Zurgaz Corebreaker"] .. FONT_COLOR_CODE_CLOSE, -- Zurgaz Corebreaker
[73995] = "|cffe4f2f5" .. L["Gaelzion"] .. FONT_COLOR_CODE_CLOSE, -- Gaelzion -- Air
[74007] = "|cffe4f2f5" .. L["Karantun"] .. FONT_COLOR_CODE_CLOSE, -- Karantun
[74022] = "|cffe4f2f5" .. L["Pipspark Thundersnap"] .. FONT_COLOR_CODE_CLOSE, -- Pipspark Thundersnap
[74038] = "|cffe4f2f5" .. L["Voraazka"] .. FONT_COLOR_CODE_CLOSE, -- Voraazka
},
separateLines = {
[1] = ORANGE_FONT_COLOR_CODE .. L["Fire"] .. FONT_COLOR_CODE_CLOSE,
[5] = "|cff42a4f5" .. L["Water"] .. FONT_COLOR_CODE_CLOSE,
[9] = YELLOW_FONT_COLOR_CODE .. L["Earth"] .. FONT_COLOR_CODE_CLOSE,
[13] = "|cffe4f2f5" .. L["Air"] .. FONT_COLOR_CODE_CLOSE,
},
},
-- Sparks of Life
['df-sparks-of-life'] = {
type = 'any',
expansion = 9,
index = 10,
name = L["Sparks of Life"],
questID = {
72646, -- The Waking Shores
72647, -- Ohn'ahran Plains
72648, -- The Azure Span
72649, -- Thaldraszus
},
reset = 'weekly',
persists = false,
fullObjective = false,
},
-- A Worthy Ally: Loamm Niffen
['df-a-worthy-ally-loamm-niffen'] = {
type = 'single',
expansion = 9,
index = 11,
name = L["A Worthy Ally: Loamm Niffen"],
questID = 75665,
reset = 'weekly',
persists = true,
fullObjective = false,
},
-- Fighting is Its Own Reward
['df-fighting-is-its-own-reward'] = {
type = 'single',
expansion = 9,
index = 12,
name = L["Fighting is Its Own Reward"],
questID = 76122,
reset = 'weekly',
persists = true,
fullObjective = false,
},
-- Researchers Under Fire
['df-researchers-under-fire'] = {
type = 'list',
expansion = 9,
index = 13,
name = L["Researchers Under Fire"],
questID = {
75630, -- Epic
75629, -- Rare
75628, -- Uncommon
75627, -- Common
},
reset = 'weekly',
persists = false,
progress = false,
onlyOnOrCompleted = false,
questName = {
[75630] = MAW_BUFF_QUALITY_STRING_EPIC, -- Epic
[75629] = MAW_BUFF_QUALITY_STRING_RARE, -- Rare
[75628] = MAW_BUFF_QUALITY_STRING_UNCOMMON, -- Uncommon
[75627] = MAW_BUFF_QUALITY_STRING_COMMON, -- Common
},
},
-- Disciple of Fyrakk
['df-disciple-of-fyrakk'] = {
type = 'single',
expansion = 9,
index = 14,
name = L["Disciple of Fyrakk"],
questID = 75467,
reset = 'weekly',
persists = false,
fullObjective = false,
},
-- Secured Shipment
['df-secured-shipment'] = {
type = 'any',
expansion = 9,
index = 15,
name = L["Secured Shipment"],
questID = {
75525,
74526,
},
reset = 'weekly',
persists = false,
fullObjective = false,
},
-- Timerift
['df-time-rift'] = {
type = 'single',
expansion = 9,
index = 16,
name = L["When Time Needs Mending"],
questID = 77236,
reset = 'weekly',
persists = false,
fullObjective = false,
},
}
---update the progress of quest to the store
---@param store QuestStore
---@param questID number
---@return boolean show is completed or on quest
local function UpdateQuestStore(store, questID)
wipe(store)
if C_QuestLog.IsQuestFlaggedCompleted(questID) then
store.show = true
store.isComplete = true
return true
elseif not C_QuestLog.IsOnQuest(questID) then
store.show = false
return false
else
local showText
local allFinished = true
local leaderboardCount = C_QuestLog.GetNumQuestObjectives(questID)
for i = 1, leaderboardCount do
local text, objectiveType, finished, numFulfilled, numRequired = GetQuestObjectiveInfo(questID, i, false)
---@cast text string
---@cast objectiveType "item"|"object"|"monster"|"reputation"|"log"|"event"|"player"|"progressbar"
---@cast finished boolean
---@cast numFulfilled number
---@cast numRequired number
allFinished = allFinished and finished
local objectiveText
if objectiveType == 'progressbar' then
numFulfilled = GetQuestProgressBarPercent(questID)
numRequired = 100
objectiveText = floor(numFulfilled or 0) .. "%"
else
objectiveText = numFulfilled .. "/" .. numRequired
end
store[i] = text
if i == 1 then
store.objectiveType = objectiveType
store.numFulfilled = numFulfilled
store.numRequired = numRequired
showText = objectiveText
else
showText = showText .. ' ' .. objectiveText
end
end
store.show = true
store.isComplete = false
store.isFinish = allFinished
store.leaderboardCount = leaderboardCount
store.text = showText
return true
end
end
---reset the progress of quest to the store
---@param store QuestStore
---@param persists boolean
local function ResetQuestStore(store, persists)
if not store.show or store.isComplete or not persists then
-- the store should be wiped if any of the following conditions are met:
-- 1. is not on quest
-- 2. is completed
-- 3. is not persistent
wipe(store)
store.show = false
end
end
---show the progress of quest
---@param store QuestStore
---@param entry SingleQuestEntry|AnyQuestEntry
---@return string?
local function ShowQuestStore(store, entry)
if not store.show then
return
elseif store.isComplete then
return SI.questCheckMark
elseif store.isFinish then
return SI.questTurnin
elseif entry.fullObjective then
return store.text
elseif store.objectiveType == 'progressbar' and store.numFulfilled then
return store.numFulfilled .. "%"
elseif store.numFulfilled and store.numRequired then
return store.numFulfilled .. "/" .. store.numRequired
end
end
---show the progress of quest list
---@param store QuestListStore
---@param entry QuestListEntry
---@return string?
local function ShowQuestListStore(store, entry)
if not store.show then
return
end
if entry.questAbbr then
for _, questID in ipairs(entry.questID) do
if store[questID].isComplete and entry.questAbbr[questID] then
return entry.questAbbr[questID]
end
end
end
local completed = 0
local total = entry.threshold or #entry.questID
for _, questID in ipairs(entry.questID) do
if store[questID].isComplete then
completed = completed + 1
end
end
return completed .. "/" .. total
end
---handle tooltip of quest
local function TooltipQuestStore(_, arg)
local store, entry, toon = unpack(arg)
---@cast store QuestStore
---@cast entry SingleQuestEntry|AnyQuestEntry
---@cast toon string
local tip = Tooltip:AcquireIndicatorTip(2, 'LEFT', 'RIGHT')
tip:AddHeader(SI:ClassColorToon(toon), entry.name)
if store.isComplete then
tip:AddLine(SI.questCheckMark)
elseif store.isFinish then
tip:AddLine(SI.questTurnin)
elseif store.leaderboardCount and store.leaderboardCount > 0 then
for i = 1, store.leaderboardCount do
tip:AddLine("")
tip:SetCell(i + 1, 1, store[i], nil, 'LEFT', 2)
end
end
tip:Show()
end
---handle tooltip of quest list
local function TooltipQuestListStore(_, arg)
local store, entry, toon = unpack(arg)
---@cast store QuestListStore
---@cast entry QuestListEntry
---@cast toon string
local tip = Tooltip:AcquireIndicatorTip(2, 'LEFT', 'RIGHT')
tip:AddHeader(SI:ClassColorToon(toon), entry.name)
local completed = 0
local total = entry.threshold or #entry.questID
for _, questID in ipairs(entry.questID) do
if store[questID].isComplete then
completed = completed + 1
end
end
tip:AddLine("", completed .. "/" .. total)
for i, questID in ipairs(entry.questID) do
if entry.separateLines and entry.separateLines[i] then
tip:AddLine(entry.separateLines[i])
end
if not entry.onlyOnOrCompleted or store[questID].show then
local questName = entry.questName and entry.questName[questID] or SI:QuestInfo(questID)
local questText
if entry.progress then
if not store.show then
-- do nothing
elseif store[questID].isComplete then
questText = SI.questCheckMark
elseif store[questID].isFinish then
questText = SI.questTurnin
elseif store[questID].objectiveType == 'progressbar' and store[questID].numFulfilled then
questText = store[questID].numFulfilled .. "%"
elseif store[questID].numFulfilled and store[questID].numRequired then
questText = store[questID].numFulfilled .. "/" .. store[questID].numRequired
end
else
questText = (
store[questID].isComplete and
(RED_FONT_COLOR_CODE .. CRITERIA_COMPLETED .. FONT_COLOR_CODE_CLOSE) or
(GREEN_FONT_COLOR_CODE .. AVAILABLE .. FONT_COLOR_CODE_CLOSE)
)
end
tip:AddLine(questName or questID, questText or "")
end
end
tip:Show()
end
---wrap tooltip of custom entry
local function TooltipCustomEntry(_, arg)
local store, entry, toon = unpack(arg)
---@cast store table
---@cast entry CustomEntry
---@cast toon string
if entry.tooltipFunc then
entry.tooltipFunc(store, entry, toon)
end
end
function Module:OnInitialize()
if not SI.db.Progress then
SI.db.Progress = {
Enable = {},
Order = {},
User = {},
}
end
for key in pairs(presets) do
if type(SI.db.Progress.Enable[key]) ~= 'boolean' then
SI.db.Progress.Enable[key] = true
end
if type(SI.db.Progress.Order[key]) ~= 'number' then
SI.db.Progress.Order[key] = 50
end
end
for key in pairs(SI.db.Progress.User) do
if type(SI.db.Progress.Enable[key]) ~= 'boolean' then
SI.db.Progress.Enable[key] = true
end
if type(SI.db.Progress.Order[key]) ~= 'number' then
SI.db.Progress.Order[key] = 50
end
end
local map = {
[1] = 'great-vault-pvp', -- PvP Conquest
[2] = 'bfa-island', -- Island Expedition
[3] = 'bfa-horrific-vision', -- Horrific Vision
[4] = 'bfa-nzoth-assault', -- N'Zoth Assaults
[5] = 'bfa-lesser-vision', -- Lesser Visions of N'Zoth
[7] = 'sl-covenant-assault', -- Covenant Assaults
[8] = 'the-world-awaits', -- The World Awaits
[9] = 'emissary-of-war', -- Emissary of War
[10] = 'sl-patterns-within-patterns', -- Patterns Within Patterns
[11] = 'df-renown', -- Dragonflight Renown
[12] = 'df-aiding-the-accord', -- Aiding the Accord
[13] = 'df-community-feast', -- Community Feast
[14] = 'df-siege-on-dragonbane-keep', -- Siege on Dragonbane Keep
[15] = 'df-grand-hunt', -- Grand Hunt
[16] = 'df-trial-of-elements', -- Trial of Elements
[17] = 'df-trial-of-flood', -- Trial of Flood
[18] = 'df-primal-storms-core', -- Primal Storms Core
[19] = 'df-primal-storms-elementals', -- Primal Storms Elementals
[20] = 'df-sparks-of-life', -- Sparks of Life
[21] = 'df-a-worthy-ally-loamm-niffen', -- A Worthy Ally: Loamm Niffen
[22] = 'df-fighting-is-its-own-reward', -- Fighting is Its Own Reward
[23] = 'df-time-rift', -- When Time Needs Mending
}
for i = 1, 22 do
-- enable status migration
if SI.db.Tooltip['Progress' .. i] ~= nil and map[i] then
SI.db.Progress.Enable[map[i]] = SI.db.Tooltip['Progress' .. i]
end
SI.db.Tooltip['Progress' .. i] = nil
end
for _, db in pairs(SI.db.Toons) do
if db.Progress then
-- old database migration
for oldKey, newKey in pairs(map) do
if db.Progress[oldKey] then
db.Progress[newKey] = db.Progress[oldKey]
db.Progress[oldKey] = nil
end
end
-- database cleanup
for key in pairs(db.Progress) do
if not presets[key] and not SI.db.Progress.User[key] then
db.Progress[key] = nil
else
-- check store type
local entry = presets[key] or SI.db.Progress.User[key]
local store = db.Progress[key]
if type(store) ~= 'nil' then
-- store contains somethings
if entry.type == 'list' then
---@cast entry QuestListEntry
if type(store) ~= 'table' then
-- broken store, should be table
db.Progress[key] = {}
end
for _, questID in ipairs(entry.questID) do
if store[questID] == true then
-- simple boolean for list entry
store[questID] = {
show = true,
}
elseif type(store[questID]) ~= 'table' then
-- broken store or false, should be table or nil
store[questID] = nil
end
end
elseif entry.type ~= 'custom' then
---@cast entry SingleQuestEntry|AnyQuestEntry
if type(store) ~= 'table' then
-- broken store, should be table
db.Progress[key] = {}
end
end
end
end
end
end
end
self.display = {}
self.displayAll = {}
self:BuildDisplayOrder()
end
function Module:OnEnable()
self:RegisterEvent('PLAYER_ENTERING_WORLD', 'UpdateAll')
self:RegisterEvent('QUEST_LOG_UPDATE', 'UpdateAll')
self:UpdateAll()
end
---sort entry
---@param left string
---@param right string
---@return boolean
local function sortDisplay(left, right)
-- sort display by order, then presets over user, then expansion, then index, then key
local leftOrder = SI.db.Progress.Order[left] or 50
local rightOrder = SI.db.Progress.Order[right] or 50
if leftOrder ~= rightOrder then
return leftOrder < rightOrder
end
local leftPreset = not not presets[left]
local rightPreset = not not presets[right]
if leftPreset ~= rightPreset then
return leftPreset
end
local leftEntry = presets[left] or SI.db.Progress.User[left]
local rightEntry = presets[right] or SI.db.Progress.User[right]
if (leftEntry.expansion or -1) ~= (rightEntry.expansion or -1) then
return (leftEntry.expansion or -1) < (rightEntry.expansion or -1)
end
if (leftEntry.index or 0) ~= (rightEntry.index or 0) then
return (leftEntry.index or 0) < (rightEntry.index or 0)
end
return left < right
end
function Module:BuildDisplayOrder()
wipe(self.display)
wipe(self.displayAll)
for key in pairs(presets) do
if SI.db.Progress.Enable[key] then
tinsert(self.display, key)
end
tinsert(self.displayAll, key)
end
for key in pairs(SI.db.Progress.User) do
if SI.db.Progress.Enable[key] then
tinsert(self.display, key)
end
tinsert(self.displayAll, key)
end
sort(self.display, sortDisplay)
sort(self.displayAll, sortDisplay)
end
---update progress entry
---@param key string
---@param entry ProgressEntry
function Module:UpdateEntry(key, entry)
local db = SI.db.Toons[SI.thisToon].Progress
if not db[key] then
db[key] = {}
end
local store = db[key]
if entry.type == 'single' then
---@cast entry SingleQuestEntry
---@cast store QuestStore
UpdateQuestStore(store, entry.questID)
elseif entry.type == 'any' then
---@cast entry AnyQuestEntry
---@cast store QuestStore
for _, questID in ipairs(entry.questID) do
local show = UpdateQuestStore(store, questID)
if show then
break
end
end
elseif entry.type == 'list' then
---@cast entry QuestListEntry
---@cast store QuestListStore
wipe(store)
if entry.unlockQuest then
store.show = C_QuestLog.IsQuestFlaggedCompleted(entry.unlockQuest)
else
store.show = true
end
for _, questID in ipairs(entry.questID) do
store[questID] = {}
UpdateQuestStore(store[questID], questID)
end
elseif entry.type == 'custom' then
---@cast entry CustomEntry
entry.func(store, entry)
end
end
function Module:UpdateAll()
for key, entry in pairs(presets) do
self:UpdateEntry(key, entry)
end
for key, entry in pairs(SI.db.Progress.User) do
self:UpdateEntry(key, entry)
end
end
---reset progress entry
---@param key string
---@param entry ProgressEntry
---@param toon string
function Module:ResetEntry(key, entry, toon)
local store = SI.db.Toons[toon].Progress and SI.db.Toons[toon].Progress[key]
if not store then return end
if entry.type == 'single' then
---@cast entry SingleQuestEntry
---@cast store QuestStore
ResetQuestStore(store, entry.persists)
elseif entry.type == 'any' then
---@cast entry AnyQuestEntry
---@cast store QuestStore
ResetQuestStore(store, entry.persists)
elseif entry.type == 'list' then
---@cast entry QuestListEntry
---@cast store QuestListStore
for _, questID in ipairs(entry.questID) do
if store[questID] then
ResetQuestStore(store[questID], entry.persists)
end
end
elseif entry.type == 'custom' then
---@cast entry CustomEntry
if entry.resetFunc then
entry.resetFunc(store, entry)
end
end
end
function Module:OnDailyReset(toon)
for key, entry in pairs(presets) do
if entry.reset == 'daily' then
self:ResetEntry(key, entry, toon)
end
end
for key, entry in pairs(SI.db.Progress.User) do
if entry.reset == 'daily' then
self:ResetEntry(key, entry, toon)
end
end
end
function Module:OnWeeklyReset(toon)
for key, entry in pairs(presets) do
if entry.reset == 'weekly' then
self:ResetEntry(key, entry, toon)
end
end
for key, entry in pairs(SI.db.Progress.User) do
if entry.reset == 'weekly' then
self:ResetEntry(key, entry, toon)
end
end
end
do
local randomSource = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
'U', 'V', 'W', 'X', 'Y', 'Z',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
}
local randomUID = function()
local result = ''
for _ = 1, 11 do
result = result .. randomSource[random(1, #randomSource)]
end
return result
end
local orderValidate = function(_, value)
if strfind(value, '^%s*[0-9]?[0-9]?[0-9]%s*$') then
return true
else
local err = L["Order must be a number in [0 - 999]"]
SI:ChatMsg(err)
return err
end
end
local options
---Add user entry to option
---@param key string
---@param entry ProgressEntry
local function AddUserEntryToOption(key, entry)
options.args.Enable.args.User.args[key] = {
order = entry.index,
type = 'toggle',
name = entry.name,
}
options.args.Sorting.args[key] = {
order = function() return tIndexOf(Module.displayAll, key) end,
type = 'input',
name = entry.name,
desc = L["Sort Order"],
validate = orderValidate,
}
options.args.User.args[key] = {
order = entry.index,
type = 'group',
name = entry.name,
get = function(info) return SI.db.Progress.User[key][info[#info]] end,
set = function(info, value) SI.db.Progress.User[key][info[#info]] = value end,
args = {
name = {
order = 1,
type = 'input',
name = L["Quest Name"],
},
questID = {
order = 2,
type = 'input',
name = L["Quest ID"],
validate = function(info, value)
local number = tonumber(value)
return number and number == floor(number)
end,
get = function(info)
return tostring(SI.db.Progress.User[key][info[#info]])
end,
set = function(info, value)
SI.db.Progress.User[key][info[#info]] = tonumber(value) or 0
Module:CleanUserEntryStore(key)
Module:UpdateEntry(key, SI.db.Progress.User[key])
end,
},
reset = {
order = 3,
type = 'select',
name = L["Quest Reset Type"],
values = {
['none'] = NONE,
['daily'] = DAILY,
['weekly'] = WEEKLY,
},
},
persists = {
order = 4,
type = 'toggle',
name = L["Progress Persists"],
},
fullObjective = {
order = 5,
type = 'toggle',
name = L["Full Objective"],
},
space = {
order = 6,
type = 'description',
name = '',
},
DeleteEntry = {
order = 7,
type = 'execute',
name = L["Delete Entry"],
func = function() Module:DeleteUserEntry(key) end,
},
},
}
end
---Clean store of user entry
---@param key string
function Module:CleanUserEntryStore(key)
for _, db in pairs(SI.db.Toons) do
if db.Progress and db.Progress[key] then
db.Progress[key] = nil
end
end
end
---Add user entry
---@param entry ProgressEntry
---@return string key
function Module:AddUserEntry(entry)
local maxIndex = 0
for _, oldEntry in pairs(SI.db.Progress.User) do
if oldEntry.index > maxIndex then
maxIndex = oldEntry.index
end
end
local data = CopyTable(entry)
data.index = maxIndex + 1
local key = 'user-' .. randomUID()
while SI.db.Progress.User[key] do
key = 'user-' .. randomUID()
end
SI.db.Progress.User[key] = data
SI.db.Progress.Order[key] = 50
SI.db.Progress.Enable[key] = true
AddUserEntryToOption(key, data)
Module:BuildDisplayOrder()
Module:UpdateEntry(key, data)
return key
end
---Delete user entry
---@param key string
function Module:DeleteUserEntry(key)
-- clean up database
SI.db.Progress.User[key] = nil
SI.db.Progress.Order[key] = nil
SI.db.Progress.Enable[key] = nil
Module:CleanUserEntryStore(key)
-- remove from options
options.args.Enable.args.User.args[key] = nil
options.args.Sorting.args[key] = nil
options.args.User.args[key] = nil
Module:BuildDisplayOrder()
end
function Module:BuildOptions(order)
---@type SingleQuestEntry
local userSingleEntry = {
type = 'single',
name = '',
questID = 0,
reset = "none",
persists = false,
fullObjective = false,
}
local userSingleEntryValidate = function()
if #(userSingleEntry.name) > 0 and userSingleEntry.questID and userSingleEntry.questID > 0 then
return true
end
end
options = {
order = order,
type = 'group',
childGroups = 'tab',
name = L["Quest progresses"],
args = {
Enable = {
order = 1,
type = 'group',
name = ENABLE,
get = function(info) return SI.db.Progress.Enable[info[#info]] end,
set = function(info, value) SI.db.Progress.Enable[info[#info]] = value; Module:BuildDisplayOrder() end,
args = {
Presets = {
order = 1,
type = 'group',
name = L["Presets"],
guiInline = true,
args = {
General = {
order = 0,
type = 'header',
name = GENERAL,
},
},
},
User = {
order = 2,
type = 'group',
name = L["User"],
hidden = function() return not next(SI.db.Progress.User) end,
guiInline = true,
args = {},
},
},
},
Sorting = {
order = 2,
type = 'group',
name = L["Sorting"],
get = function(info) return tostring(SI.db.Progress.Order[info[#info]]) end,
set = function(info, value) SI.db.Progress.Order[info[#info]] = tonumber(value) or 50; Module:BuildDisplayOrder() end,
args = {},
},
User = {
order = 3,
type = 'group',
name = L["User"],
args = {
New = {
order = -1,
type = 'group',
name = L["New Single Quest"],
get = function(info) return userSingleEntry[info[#info]] end,
set = function(info, value) userSingleEntry[info[#info]] = value end,
args = {
name = {
order = 1,
type = 'input',
name = L["Quest Name"],
},
questID = {
order = 2,
type = 'input',
name = L["Quest ID"],
validate = function(info, value)
local number = tonumber(value)
return number and number == floor(number)
end,
get = function(info) return tostring(userSingleEntry[info[#info]]) end,
set = function(info, value) userSingleEntry[info[#info]] = tonumber(value) or 0 end,
},
reset = {
order = 3,
type = 'select',
name = L["Quest Reset Type"],
values = {
['none'] = NONE,
['daily'] = DAILY,
['weekly'] = WEEKLY,
},
},
persists = {
order = 4,
type = 'toggle',
name = L["Progress Persists"],
},
fullObjective = {
order = 5,
type = 'toggle',
name = L["Full Objective"],
},
space = {
order = 6,
type = 'description',
name = '',
},
AddEntry = {
order = 7,
type = 'execute',
name = L["Add Entry"],
disabled = function() return not userSingleEntryValidate() end,
func = function() Module:AddUserEntry(userSingleEntry) end,
},
CleanEntry = {
order = 8,
type = 'execute',
name = L["Clean Entry"],
func = function()
userSingleEntry.name = ''
userSingleEntry.questID = 0
userSingleEntry.reset = 'none'
userSingleEntry.persists = false
userSingleEntry.fullObjective = false
end,
},
},
},
},
},
},
}
for key, entry in pairs(presets) do
if entry.expansion then
if not options.args.Enable.args.Presets.args['Expansion' .. entry.expansion .. 'Header'] then
options.args.Enable.args.Presets.args['Expansion' .. entry.expansion .. 'Header'] = {
order = (entry.expansion + 1) * 100,
type = 'header',
name = _G['EXPANSION_NAME' .. entry.expansion],
}
end
end
options.args.Enable.args.Presets.args[key] = {
order = ((entry.expansion or -1) + 1) * 100 + entry.index,
type = 'toggle',
name = entry.name,
}
options.args.Sorting.args[key] = {
order = function() return tIndexOf(Module.displayAll, key) end,
type = 'input',
name = entry.name,
desc = L["Sort Order"],
validate = orderValidate,
}
end
for key, entry in pairs(SI.db.Progress.User) do
AddUserEntryToOption(key, entry)
end
return options
end
end
---reset progress entry
---@param entry ProgressEntry
---@param questID number
function Module:IsEntryContainsQuest(entry, questID)
if entry.type == 'single' then
---@cast entry SingleQuestEntry
return entry.questID == questID
elseif entry.type == 'list' or entry.type == 'list' then
---@cast entry AnyQuestEntry|QuestListEntry
return tContains(entry.questID, questID)
elseif entry.type == 'custom' and entry.relatedQuest then
---@cast entry CustomEntry
return tContains(entry.relatedQuest, questID)
end
end
function Module:QuestEnabled(questID)
for key, entry in pairs(presets) do
if SI.db.Progress.Enable[key] and self:IsEntryContainsQuest(entry, questID) then
return true
end
end
for key, entry in pairs(SI.db.Progress.User) do
if SI.db.Progress.Enable[key] and self:IsEntryContainsQuest(entry, questID) then
return true
end
end
end
function Module:ShowTooltip(tooltip, columns, showall, preshow)
local cpairs = SI.cpairs
local first = true
for _, key in ipairs(showall and self.displayAll or self.display) do
local entry = presets[key] or SI.db.Progress.User[key]
local show = false
for _, t in cpairs(SI.db.Toons, true) do
local store = t.Progress and t.Progress[key]
if (
showall or
(entry.type ~= 'custom' and store and store.show) or
(entry.type == 'custom' and store and entry.showFunc(store, entry))
) then
show = true
break
end
end
if show then
if first then
preshow()
first = false
end
local line = tooltip:AddLine(NORMAL_FONT_COLOR_CODE .. entry.name .. FONT_COLOR_CODE_CLOSE)
for toon, t in cpairs(SI.db.Toons, true) do
local store = t.Progress and t.Progress[key]
-- check if current toon is showing
-- don't add columns
if store and columns[toon .. 1] then
---@cast store table|QuestStore|QuestListStore
local text, hoverFunc, hoverArg
if entry.type == 'custom' then
---@cast entry CustomEntry
---@cast store table
text = entry.showFunc(store, entry)
if entry.tooltipFunc then
hoverFunc = TooltipCustomEntry
hoverArg = {store, entry, toon}
end
elseif entry.type == 'single' or entry.type == 'any' then
---@cast entry SingleQuestEntry|AnyQuestEntry
---@cast store QuestStore
text = ShowQuestStore(store, entry)
if entry.fullObjective then
hoverFunc = TooltipQuestStore
hoverArg = {store, entry, toon}
end
elseif entry.type == 'list' then
---@cast entry QuestListEntry
---@cast store QuestListStore
text = ShowQuestListStore(store, entry)
hoverFunc = TooltipQuestListStore
hoverArg = {store, entry, toon}
end
if text then
local col = columns[toon .. 1]
tooltip:SetCell(line, col, text, 'CENTER', 4)
if hoverFunc then
tooltip:SetCellScript(line, col, 'OnEnter', hoverFunc, hoverArg)
tooltip:SetCellScript(line, col, 'OnLeave', Tooltip.CloseIndicatorTip)
end
end
end
end
end
end
end