Untitled

mail@pastecode.io avatar
unknown
plain_text
a year ago
50 kB
3
Indexable
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