v9.7.2: Ebonhold Database integration and core logic refinements

This commit is contained in:
Xurkon
2026-02-14 21:13:44 -06:00
parent 0a2cc15641
commit 6ef85d4e2d
573 changed files with 1751730 additions and 2 deletions
+115
View File
@@ -0,0 +1,115 @@
---@type QuestieJourney
local QuestieJourney = QuestieLoader:CreateModule("QuestieJourney")
local _QuestieJourney = QuestieJourney.private
---@type QuestieDB
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
---@type l10n
local l10n = QuestieLoader:ImportModule("l10n")
--- COMPATIBILITY ---
local CALENDAR_FULLDATE_MONTH_NAMES = QuestieCompat.CALENDAR_FULLDATE_MONTH_NAMES
function _QuestieJourney:GetHistory()
local journeyEntries = _QuestieJourney:GetJourneyEntries()
local years = {}
for k in pairs(journeyEntries) do
table.insert(years, k)
end
table.sort(years)
local history = {}
for _, year in pairs(years) do
local yearTable = {
value = year,
text = l10n('Year %s', year),
children = {},
}
for month=12, 1, -1 do -- Iterate the month from last to newest
if journeyEntries[year][month] then -- Only check month with events
local monthView = {
value = month,
text = CALENDAR_FULLDATE_MONTH_NAMES[month] .. ' '.. year,
children = {},
}
for entryIndex=#journeyEntries[year][month], 1, -1 do -- Iterate backwards to show newest first
---@type JourneyEntry
local entry = journeyEntries[year][month][entryIndex]
local entryIdx = entry.idx
local entryText = _QuestieJourney:GetEntryText(entry.value)
local entryView = {
value = entryIdx,
text = entryText,
}
tinsert(monthView.children, entryView)
end
tinsert(yearTable.children, monthView)
end
end
tinsert(history, yearTable)
end
return history
end
--- Get a sorted copy of the journey entries.
---@return table<string, table<string, SortedJourneyEntry>>
function _QuestieJourney:GetJourneyEntries()
local dateTable = {}
-- -- Sort all of the entries by year and month
-- ---@param v JourneyEntry
for i, v in ipairs(Questie.db.char.journey) do
local year = tonumber(date('%Y', v.Timestamp))
if (not dateTable[year]) then
dateTable[year] = {}
end
local month = tonumber(date('%m', v.Timestamp))
if (not dateTable[year][month]) then
dateTable[year][month] = {}
end
---@class SortedJourneyEntry
local entry = {}
---@type number
entry.idx = i
---@type JourneyEntry
entry.value = v
tinsert(dateTable[year][month], entry)
end
return dateTable
end
function _QuestieJourney:GetEntryText(entry)
local entryText = ""
if entry.Event == "Level" then
entryText = l10n('You Reached Level %s', entry.NewLevel)
elseif entry.Event == "Note" then
entryText = l10n('Note: %s', entry.Title)
elseif entry.Event == "Quest" then
local state
if entry.SubType == "Accept" then
state = l10n('Accepted')
elseif entry.SubType == "Complete" then
state = l10n('Completed')
elseif entry.SubType == "Abandon" then
state = l10n('Abandoned')
else
state = "ERROR!!"
end
local qName = QuestieDB.QueryQuestSingle(entry.Quest, "name")
entryText = l10n('Quest %s: %s', state or "no state", qName or "no quest name")
end
return entryText
end
+423
View File
@@ -0,0 +1,423 @@
---@type QuestieJourney
local QuestieJourney = QuestieLoader:CreateModule("QuestieJourney")
local _QuestieJourney = QuestieJourney.private
-------------------------
--Import modules.
-------------------------
---@type QuestieJourneyUtils
local QuestieJourneyUtils = QuestieLoader:ImportModule("QuestieJourneyUtils")
---@type QuestieDB
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
---@type QuestieLib
local QuestieLib = QuestieLoader:ImportModule("QuestieLib")
---@type l10n
local l10n = QuestieLoader:ImportModule("l10n")
local AceGUI = LibStub("AceGUI-3.0")
-- TODO remove again once the call in manageZoneTree was removed
---@param container ScrollFrame
---@param quest Quest
function _QuestieJourney:DrawQuestDetailsFrame(container, quest)
local questNameHeader = _QuestieJourney:CreateHeading(quest.name, true)
container:AddChild(questNameHeader)
QuestieJourneyUtils:Spacer(container)
local obj = AceGUI:Create("Label")
obj:SetText(_QuestieJourney:CreateObjectiveText(quest.Description))
obj:SetFullWidth(true)
container:AddChild(obj)
QuestieJourneyUtils:Spacer(container)
local questInfoHeader = _QuestieJourney:CreateHeading(l10n('Quest Information'), true)
container:AddChild(questInfoHeader)
-- Generic Quest Information
local levelLabel = _QuestieJourney:CreateLabel(Questie:Colorize(l10n('Recommended Quest Level: '), 'yellow') .. quest.level, true)
container:AddChild(levelLabel)
local minLevelLabel = _QuestieJourney:CreateLabel(Questie:Colorize(l10n('Minimum Required Level for Quest: '), 'yellow') .. quest.requiredLevel, true)
container:AddChild(minLevelLabel)
local levelDiffString = _QuestieJourney:GetDifficultyString(quest.level, quest.requiredLevel)
local levelDiffLabel = _QuestieJourney:CreateLabel(levelDiffString, true)
container:AddChild(levelDiffLabel)
local questIdLabel = _QuestieJourney:CreateLabel(Questie:Colorize(l10n('Quest ID: '), 'yellow') .. quest.Id, true)
container:AddChild(questIdLabel)
QuestieJourneyUtils:Spacer(container)
local preQuestCounter, preQuestInlineGroup = _QuestieJourney:CreatePreQuestGroup(quest)
if preQuestCounter > 1 then -- Don't add the group if it doesn't contain a pre quest
QuestieJourneyUtils:Spacer(preQuestInlineGroup)
container:AddChild(preQuestInlineGroup)
end
-- Get Quest Start NPC
if quest.Starts and quest.Starts.NPC then
local startNPCGroup = AceGUI:Create("InlineGroup")
startNPCGroup:SetLayout("List")
startNPCGroup:SetTitle(l10n('Quest Start NPC Information'))
startNPCGroup:SetFullWidth(true)
container:AddChild(startNPCGroup)
QuestieJourneyUtils:Spacer(startNPCGroup)
local startNpc = QuestieDB:GetNPC(quest.Starts.NPC[1])
local startNPCNameLabel = _QuestieJourney:CreateLabel(startNpc.name, true)
startNPCNameLabel:SetFontObject(GameFontHighlight)
startNPCNameLabel:SetColor(255, 165, 0)
startNPCGroup:AddChild(startNPCNameLabel)
local startNPCZoneLabel = AceGUI:Create("Label")
local startindex = 0
if (not startNpc.spawns) then
return
end
for i in pairs(startNpc.spawns) do
startindex = i
end
if startindex == 0 then
return
end
local continent = QuestieJourneyUtils:GetZoneName(startindex)
startNPCZoneLabel:SetText(l10n(continent))
startNPCZoneLabel:SetFullWidth(true)
startNPCGroup:AddChild(startNPCZoneLabel)
local startx = startNpc.spawns[startindex][1][1]
local starty = startNpc.spawns[startindex][1][2]
if (startx ~= -1 or starty ~= -1) then
local startNPCLocLabel = AceGUI:Create("Label")
startNPCLocLabel:SetText("X: ".. startx .." || Y: ".. starty)
startNPCLocLabel:SetFullWidth(true)
startNPCGroup:AddChild(startNPCLocLabel)
end
local startNPCIdLabel = AceGUI:Create("Label")
startNPCIdLabel:SetText("NPC ID: ".. startNpc.id)
startNPCIdLabel:SetFullWidth(true)
startNPCGroup:AddChild(startNPCIdLabel)
QuestieJourneyUtils:Spacer(startNPCGroup)
-- Also Starts
if startNpc.questStarts then
local alsoStartsLabel = AceGUI:Create("Label")
alsoStartsLabel:SetText(l10n('This NPC Also Starts the following quests:'))
alsoStartsLabel:SetColor(255, 165, 0)
alsoStartsLabel:SetFontObject(GameFontHighlight)
alsoStartsLabel:SetFullWidth(true)
startNPCGroup:AddChild(alsoStartsLabel)
local startQuests = {}
local counter = 1
for _, v in pairs(startNpc.questStarts) do
if v ~= quest.Id then
startQuests[counter] = {}
local startQuest = QuestieDB.GetQuest(v)
local label = _QuestieJourney:GetInteractiveQuestLabel(startQuest)
startQuests[counter].frame = label
startQuests[counter].quest = startQuest
startNPCGroup:AddChild(label)
counter = counter + 1
end
end
if #startQuests == 0 then
local noQuestLabel = AceGUI:Create("Label")
noQuestLabel:SetText(l10n('No Quests to List'))
noQuestLabel:SetFullWidth(true)
startNPCGroup:AddChild(noQuestLabel)
end
end
QuestieJourneyUtils:Spacer(startNPCGroup)
end
-- Get Quest Start GameObject
if quest.Starts and quest.Starts.GameObject then
local startObjectGroup = AceGUI:Create("InlineGroup")
startObjectGroup:SetLayout("List")
startObjectGroup:SetTitle(l10n('Quest Start Object Information'))
startObjectGroup:SetFullWidth(true)
container:AddChild(startObjectGroup)
QuestieJourneyUtils:Spacer(startObjectGroup)
for _, oid in pairs(quest.Starts.GameObject) do
local startObj = QuestieDB:GetObject(oid)
local startObjectNameLabel = AceGUI:Create("Label")
startObjectNameLabel:SetText(startObj.name)
startObjectNameLabel:SetFontObject(GameFontHighlight)
startObjectNameLabel:SetColor(255, 165, 0)
startObjectNameLabel:SetFullWidth(true)
startObjectGroup:AddChild(startObjectNameLabel)
local startObjectZoneLabel = AceGUI:Create("Label")
local startindex = 0
for i in pairs(startObj.spawns) do
startindex = i
end
local continent = QuestieJourneyUtils:GetZoneName(startindex)
startObjectZoneLabel:SetText(continent)
startObjectZoneLabel:SetFullWidth(true)
startObjectGroup:AddChild(startObjectZoneLabel)
local startx = startObj.spawns[startindex][1][1]
local starty = startObj.spawns[startindex][1][2]
if (startx ~= -1 or starty ~= -1) then
local startObjectLocLabel = AceGUI:Create("Label")
startObjectLocLabel:SetText("X: ".. startx .." || Y: ".. starty)
startObjectLocLabel:SetFullWidth(true)
startObjectGroup:AddChild(startObjectLocLabel)
end
local startObjectIdLabel = AceGUI:Create("Label")
startObjectIdLabel:SetText("Object ID: ".. startObj.id)
startObjectIdLabel:SetFullWidth(true)
startObjectGroup:AddChild(startObjectIdLabel)
QuestieJourneyUtils:Spacer(startObjectGroup)
-- Also Starts
if startObj.questStarts then
local alsoStartsLabel = AceGUI:Create("Label")
alsoStartsLabel:SetText(l10n('This Object Also Starts the following quests:'))
alsoStartsLabel:SetColor(255, 165, 0)
alsoStartsLabel:SetFontObject(GameFontHighlight)
alsoStartsLabel:SetFullWidth(true)
startObjectGroup:AddChild(alsoStartsLabel)
local startQuests = {}
local counter = 1
for _, v in pairs(startObj.questStarts) do
if v ~= quest.Id then
startQuests[counter] = {}
local startQuest = QuestieDB.GetQuest(v)
local label = _QuestieJourney:GetInteractiveQuestLabel(startQuest)
startQuests[counter].frame = label
startQuests[counter].quest = startQuest
startObjectGroup:AddChild(label)
counter = counter + 1
end
end
if #startQuests == 0 then
local noQuestLabel = AceGUI:Create("Label")
noQuestLabel:SetText(l10n('No Quests to List'))
noQuestLabel:SetFullWidth(true)
startObjectGroup:AddChild(noQuestLabel)
end
end
QuestieJourneyUtils:Spacer(startObjectGroup)
end
end
QuestieJourneyUtils:Spacer(container)
if quest.Finisher and quest.Finisher.Name and quest.Finisher.Type == "monster" then
local endNPCGroup = AceGUI:Create("InlineGroup")
endNPCGroup:SetLayout("Flow")
endNPCGroup:SetTitle(l10n('Quest Turn-in NPC Information'))
endNPCGroup:SetFullWidth(true)
container:AddChild(endNPCGroup)
QuestieJourneyUtils:Spacer(endNPCGroup)
local endNPC = QuestieDB:GetNPC(quest.Finisher.Id)
local endNPCNameLabel = AceGUI:Create("Label")
endNPCNameLabel:SetText(endNPC.name)
endNPCNameLabel:SetFontObject(GameFontHighlight)
endNPCNameLabel:SetColor(255, 165, 0)
endNPCNameLabel:SetFullWidth(true)
endNPCGroup:AddChild(endNPCNameLabel)
local endNPCZoneLabel = AceGUI:Create("Label")
local endindex = 0
if (not endNPC.spawns) then
return
end
for i in pairs(endNPC.spawns) do
endindex = i
end
local continent = QuestieJourneyUtils:GetZoneName(endindex)
endNPCZoneLabel:SetText(l10n(continent))
endNPCZoneLabel:SetFullWidth(true)
endNPCGroup:AddChild(endNPCZoneLabel)
if (next(endNPC.spawns)) then
local endx = endNPC.spawns[endindex][1][1]
local endy = endNPC.spawns[endindex][1][2]
if (endx ~= -1 or endy ~= -1) then
local endNPCLocLabel = AceGUI:Create("Label")
endNPCLocLabel:SetText("X: ".. endx .." || Y: ".. endy)
endNPCLocLabel:SetFullWidth(true)
endNPCGroup:AddChild(endNPCLocLabel)
end
end
local endNPCIdLabel = AceGUI:Create("Label")
endNPCIdLabel:SetText("NPC ID: ".. endNPC.id)
endNPCIdLabel:SetFullWidth(true)
endNPCGroup:AddChild(endNPCIdLabel)
QuestieJourneyUtils:Spacer(endNPCGroup)
-- Also ends
if endNPC.endQuests then
local alsoEndsLabel = AceGUI:Create("Label")
alsoEndsLabel:SetText(l10n('This NPC Also Completes the following quests:'))
alsoEndsLabel:SetFontObject(GameFontHighlight)
alsoEndsLabel:SetColor(255, 165, 0)
alsoEndsLabel:SetFullWidth(true)
endNPCGroup:AddChild(alsoEndsLabel)
local endQuests = {}
local counter = 1
for _, v in ipairs(endNPC.endQuests) do
if v ~= quest.Id then
endQuests[counter] = {}
local endQuest = QuestieDB.GetQuest(v)
local label = _QuestieJourney:GetInteractiveQuestLabel(endQuest)
endQuests[counter].frame = label
endQuests[counter].quest = endQuest
endNPCGroup:AddChild(label)
counter = counter + 1
end
end
if #endQuests == 0 then
local noQuestLabel = AceGUI:Create("Label")
noQuestLabel:SetText(l10n('No Quests to List'))
noQuestLabel:SetFullWidth(true)
endNPCGroup:AddChild(noQuestLabel)
end
QuestieJourneyUtils:Spacer(endNPCGroup)
end
-- Fix for sometimes the scroll content will max out and not show everything until window is resized
container.content:SetHeight(10000)
end
end
---@param text string
---@param fullWidth boolean
---@return AceHeader
function _QuestieJourney:CreateHeading(text, fullWidth)
---@class AceHeader
local header = AceGUI:Create("Heading")
header:SetFullWidth(fullWidth)
header:SetText(text)
return header
end
---@param text string
---@param fullWidth boolean
---@return AceLabel
function _QuestieJourney:CreateLabel(text, fullWidth)
---@class AceLabel
local header = AceGUI:Create("Label")
header:SetFullWidth(fullWidth)
header:SetText(text)
return header
end
---@param questLevel number
---@param questMinLevel number
---@return string
function _QuestieJourney:GetDifficultyString(questLevel, questMinLevel)
local red, orange, yellow, green, gray = _QuestieJourney:GetLevelDifficultyRanges(questLevel, questMinLevel)
local diffStr = ''
if red then
diffStr = diffStr .. "|cFFFF1A1A[".. red .."]|r "
end
if orange then
diffStr = diffStr .. "|cFFFF8040[".. orange .."]|r "
end
diffStr = diffStr .. "|cFFFFFF00[".. yellow .."]|r "
diffStr = diffStr .. "|cFF40C040[".. green .."]|r "
diffStr = diffStr .. "|cFFC0C0C0[".. gray .."]|r "
return Questie:Colorize(l10n('Difficulty Range: %s', diffStr), 'yellow')
end
---@param quest Quest
---@return number @The number of pre quests added to the group
---@return AceInlineGroup @The created Ace InlineGroup
function _QuestieJourney:CreatePreQuestGroup(quest)
---@class AceInlineGroup
local preQuestInlineGroup = AceGUI:Create("InlineGroup")
local preQuestCounter = 1
preQuestInlineGroup:SetLayout("List")
preQuestInlineGroup:SetTitle(l10n('Pre Quests'))
preQuestInlineGroup:SetFullWidth(true)
if (quest.preQuestSingle and next(quest.preQuestSingle)) then
for _, v in pairs(quest.preQuestSingle) do
if v ~= quest.Id then
local preQuest = QuestieDB.GetQuest(v)
local label = _QuestieJourney:GetInteractiveQuestLabel(preQuest)
preQuestInlineGroup:AddChild(label)
preQuestCounter = preQuestCounter + 1
end
end
end
if (quest.preQuestGroup and next(quest.preQuestGroup)) then
for _, v in pairs(quest.preQuestGroup) do
if v ~= quest.Id then
local preQuest = QuestieDB.GetQuest(v)
local label = _QuestieJourney:GetInteractiveQuestLabel(preQuest)
preQuestInlineGroup:AddChild(label)
preQuestCounter = preQuestCounter + 1
end
end
end
return preQuestCounter, preQuestInlineGroup
end
---@param quest Quest
---@return AceInteractiveLabel
function _QuestieJourney:GetInteractiveQuestLabel(quest)
---@class AceInteractiveLabel
local label = AceGUI:Create("InteractiveLabel")
local questId = quest.Id
label:SetText(QuestieLib:GetColoredQuestName(questId, Questie.db.profile.enableTooltipsQuestLevel, false, true))
label:SetUserData('id', questId)
label:SetUserData('name', quest.name)
label:SetCallback("OnClick", function()
ItemRefTooltip:SetHyperlink("%|Hquestie:" .. questId .. ":.*%|h", "%[%[" .. quest.level .. "%] " .. quest.name .. " %(" .. questId .. "%)%]")
end)
label:SetCallback("OnEnter", _QuestieJourney.ShowJourneyTooltip)
label:SetCallback("OnLeave", _QuestieJourney.HideJourneyTooltip)
return label
end
+217
View File
@@ -0,0 +1,217 @@
---@class QuestieJourney
local QuestieJourney = QuestieLoader:CreateModule("QuestieJourney")
local _QuestieJourney = QuestieJourney.private
QuestieJourneyFrame = nil
-------------------------
--Import modules.
-------------------------
---@type QuestiePlayer
local QuestiePlayer = QuestieLoader:ImportModule("QuestiePlayer")
---@type QuestieOptions
local QuestieOptions = QuestieLoader:ImportModule("QuestieOptions")
---@type ZoneDB
local ZoneDB = QuestieLoader:ImportModule("ZoneDB")
---@type l10n
local l10n = QuestieLoader:ImportModule("l10n")
---@type QuestieCombatQueue
local QuestieCombatQueue = QuestieLoader:ImportModule("QuestieCombatQueue")
-- Useful doc about the AceGUI TreeGroup: https://github.com/hurricup/WoW-Ace3/blob/master/AceGUI-3.0/widgets/AceGUIContainer-TreeGroup.lua
local tinsert = table.insert
QuestieJourney.continents = {}
QuestieJourney.zones = {}
QuestieJourney.tabGroup = nil
local AceGUI = LibStub("AceGUI-3.0")
local isWindowShown = false
_QuestieJourney.lastOpenWindow = "journey"
_QuestieJourney.lastZoneSelection = {}
local notesPopupWin
local notesPopupWinIsOpen = false
QuestieJourney.questCategoryKeys = {
EASTERN_KINGDOMS = 1,
KALIMDOR = 2,
OUTLAND = 3,
NORTHREND = 4,
DUNGEONS = 5,
BATTLEGROUNDS = 6,
CLASS = 7,
PROFESSIONS = 8,
EVENTS = 9,
}
function QuestieJourney:Initialize()
local continents = {}
for id, name in pairs(l10n.continentLookup) do
if not (name == "Outland" and Questie.IsClassic) and not (name == "Northrend" and (Questie.IsClassic or Questie.IsTBC)) then
continents[id] = l10n(name)
end
end
coroutine.yield()
continents[QuestieJourney.questCategoryKeys.CLASS] = QuestiePlayer:GetLocalizedClassName()
coroutine.yield()
self.continents = continents
self.zoneMap = ZoneDB:GetZonesWithQuests(true)
self.zones = ZoneDB:GetRelevantZones()
coroutine.yield()
self:BuildMainFrame()
end
function QuestieJourney:BuildMainFrame()
if not QuestieJourneyFrame then
local journeyFrame = AceGUI:Create("Frame")
journeyFrame:SetCallback("OnClose", function()
isWindowShown = false
if notesPopupWinIsOpen then
notesPopupWin:Hide()
notesPopupWin = nil
notesPopupWinIsOpen = false
end
end)
journeyFrame:SetTitle(l10n("%s's Journey", UnitName("player")))
journeyFrame:SetLayout("Fill")
journeyFrame:EnableResize(false)
QuestieCompat.SetResizeBounds(journeyFrame.frame, 550, 400)
local tabGroup = AceGUI:Create("TabGroup")
tabGroup:SetLayout("Flow")
tabGroup:SetTabs({
{
text = l10n('My Journey'),
value="journey"
},
{
text = l10n('Quests by Zone'),
value="zone"
},
{
text = l10n('Advanced Search'),
value="search"
}
})
tabGroup:SetCallback("OnGroupSelected", function(widget, _, group) _QuestieJourney:HandleTabChange(widget, group) end)
tabGroup:SelectTab("journey")
QuestieJourney.tabGroup = tabGroup
journeyFrame:AddChild(QuestieJourney.tabGroup)
local settingsButton = AceGUI:Create("Button")
settingsButton:SetWidth(160)
settingsButton:SetPoint("TOPRIGHT", journeyFrame.frame, "TOPRIGHT", -50, -13)
settingsButton:SetText(l10n('Questie Options'))
settingsButton:SetCallback("OnClick", function()
QuestieCombatQueue:Queue(function()
QuestieJourney:ToggleJourneyWindow()
QuestieOptions:OpenConfigWindow()
end)
end)
journeyFrame:AddChild(settingsButton)
journeyFrame:Hide()
QuestieJourneyFrame = journeyFrame
table.insert(UISpecialFrames, "QuestieJourneyFrame")
end
end
function QuestieJourney:IsShown()
return isWindowShown
end
function QuestieJourney:ToggleJourneyWindow()
-- There are ways to toggle this function before the frame has been created
if QuestieJourneyFrame then
if (not isWindowShown) then
PlaySound(882)
local treeGroup = _QuestieJourney:HandleTabChange(_QuestieJourney.containerCache, _QuestieJourney.lastOpenWindow)
if treeGroup then
_QuestieJourney.treeCache = treeGroup
end
QuestieJourneyFrame:Show()
isWindowShown = true
else
QuestieJourneyFrame:Hide()
isWindowShown = false
end
else
Questie:Error("QuestieJourney:ToggleJourneyWindow() called before QuestieJourneyFrame was initialized!")
end
end
function QuestieJourney:PlayerLevelUp(level)
-- Complete Quest added to Journey
---@type JourneyEntry
local entry = {
Event = "Level",
NewLevel = level,
Timestamp = time()
}
tinsert(Questie.db.char.journey, entry)
end
function QuestieJourney:AcceptQuest(questId)
-- Add quest accept journey note.
---@type JourneyEntry
local entry = {
Event = "Quest",
SubType = "Accept",
Quest = questId,
Level = QuestiePlayer.GetPlayerLevel(),
Timestamp = time()
}
tinsert(Questie.db.char.journey, entry)
end
function QuestieJourney:AbandonQuest(questId)
-- Abandon Quest added to Journey
-- first check to see if the quest has been completed already or not
local skipAbandon = false
for i in ipairs(Questie.db.char.journey) do
local entry = Questie.db.char.journey[i]
if entry.Event == "Quest" then
if entry.Quest == questId then
if entry.SubType == "Complete" then
skipAbandon = true
end
end
end
end
if not skipAbandon then
---@type JourneyEntry
local entry = {
Event = "Quest",
SubType = "Abandon",
Quest = questId,
Level = QuestiePlayer.GetPlayerLevel(),
Timestamp = time()
}
tinsert(Questie.db.char.journey, entry)
end
end
function QuestieJourney:CompleteQuest(questId)
-- Complete Quest added to Journey
---@class JourneyEntry
local entry = {
Event = "Quest",
SubType = "Complete",
Quest = questId,
Level = QuestiePlayer.GetPlayerLevel(),
Timestamp = time()
}
tinsert(Questie.db.char.journey, entry)
end
+120
View File
@@ -0,0 +1,120 @@
---@type QuestieJourney
local QuestieJourney = QuestieLoader:CreateModule("QuestieJourney")
local _QuestieJourney = QuestieJourney.private
-------------------------
--Import modules.
-------------------------
---@type QuestieSearchResults
local QuestieSearchResults = QuestieLoader:ImportModule("QuestieSearchResults")
---@type QuestieDB
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
---@type l10n
local l10n = QuestieLoader:ImportModule("l10n")
_QuestieJourney.containerCache = nil
_QuestieJourney.treeCache = nil
function _QuestieJourney:ShowJourneyTooltip()
if GameTooltip:IsShown() then
return
end
local button = self -- ACE is doing something stupid here. Don't add "self" as parameter, when you use it as "_QuestieJourney.ShowJourneyTooltip" as callback
local qid = button:GetUserData('id')
local quest = QuestieDB.GetQuest(tonumber(qid))
if quest then
GameTooltip:SetOwner(_G["QuestieJourneyFrame"].frame:GetParent(), "ANCHOR_CURSOR")
GameTooltip:AddLine("[".. quest.level .."] ".. quest.name)
GameTooltip:AddLine("|cFFFFFFFF" .. _QuestieJourney:CreateObjectiveText(quest.Description))
GameTooltip:SetFrameStrata("TOOLTIP")
GameTooltip:Show()
end
end
function _QuestieJourney:HideJourneyTooltip()
if GameTooltip:IsShown() then
GameTooltip:Hide()
end
end
function _QuestieJourney:CreateObjectiveText(desc)
local objText = ""
if desc then
if type(desc) == "table" then
for _, v in ipairs(desc) do
objText = objText .. v .. "\n"
end
else
objText = objText .. tostring(desc) .. "\n"
end
else
objText = Questie:Colorize(l10n('This quest is an automatic completion quest and does not contain an objective.'), 'yellow')
end
return objText
end
function _QuestieJourney:HandleTabChange(container, group)
if not _QuestieJourney.containerCache then
_QuestieJourney.containerCache = container
end
container:ReleaseChildren()
if group == "journey" then
local treeGroup = _QuestieJourney.myJourney:DrawTab(container)
_QuestieJourney.myJourney:ManageTree(treeGroup)
_QuestieJourney.lastOpenWindow = "journey"
return treeGroup
elseif group == "zone" then
_QuestieJourney.questsByZone:DrawTab(container)
_QuestieJourney.lastOpenWindow = "zone"
return nil
elseif group == "search" then
QuestieSearchResults:DrawSearchTab(container)
_QuestieJourney.lastOpenWindow = "search"
return nil
end
end
function _QuestieJourney:GetLevelDifficultyRanges(questLevel, questMinLevel)
local red, orange, yellow, green, gray
-- Calculate Base Values
red = questMinLevel
orange = questLevel - 4
yellow = questLevel - 2
green = questLevel + 3
-- Gray Level based on level range.
if (questLevel <= 13) then
gray = questLevel + 6
elseif (questLevel <= 39) then
gray = (questLevel + math.ceil(questLevel / 10) + 5)
else
gray = (questLevel + math.ceil(questLevel / 5) + 1)
end
-- Double check for negative values
if yellow <= 0 then
yellow = questMinLevel
end
if orange < questMinLevel then
orange = questMinLevel
end
if orange == yellow then
orange = nil
end
if red == orange or not orange then
red = nil
end
return red, orange, yellow, green, gray
end
+67
View File
@@ -0,0 +1,67 @@
---@class QuestieJourneyUtils
local QuestieJourneyUtils = QuestieLoader:CreateModule("QuestieJourneyUtils")
---@type l10n
local l10n = QuestieLoader:ImportModule("l10n")
local AceGUI = LibStub("AceGUI-3.0");
function QuestieJourneyUtils:GetSortedZoneKeys(zones)
local function compare(a, b)
return zones[a] < zones[b]
end
local zoneNames = {}
for k, _ in pairs(zones) do
table.insert(zoneNames, k)
end
table.sort(zoneNames, compare)
return zoneNames
end
function QuestieJourneyUtils:Spacer(container, size)
local spacer = AceGUI:Create("Label");
spacer:SetFullWidth(true);
spacer:SetText(" ");
if size and size == "large" then
spacer:SetFontObject(GameFontHighlightLarge);
elseif size and size == "small" then
spacer:SetFontObject(GameFontHighlightSmall);
else
spacer:SetFontObject(GameFontHighlight);
end
container:AddChild(spacer);
end
function QuestieJourneyUtils:AddLine(frame, text)
local label = AceGUI:Create("Label")
label:SetFullWidth(true);
label:SetText(text)
label:SetFontObject(GameFontNormal)
frame:AddChild(label)
end
function QuestieJourneyUtils:GetZoneName(id)
local name = l10n("Unknown Zone")
for category, data in pairs(l10n.zoneLookup) do
if data[id] then
name = l10n.zoneLookup[category][id]
break
end
end
-- Ascension can use custom UiMapIds for zones/sub-zones (e.g. 1238 Northshire Valley).
-- Those won't exist in l10n.zoneLookup (which is AreaId-based), so fallback to UiMapData / mapInfo.
if name == l10n("Unknown Zone") then
local uiMapData = QuestieCompat and QuestieCompat.UiMapData and QuestieCompat.UiMapData[id]
if uiMapData and uiMapData.name then
name = uiMapData.name
elseif QuestieCompat and QuestieCompat.C_Map and QuestieCompat.C_Map.GetMapInfo then
local mapInfo = QuestieCompat.C_Map.GetMapInfo(id)
if mapInfo and mapInfo.name then
name = mapInfo.name
end
end
end
return name
end
+217
View File
@@ -0,0 +1,217 @@
---@class QuestieSearch
local QuestieSearch = QuestieLoader:CreateModule("QuestieSearch");
---@type QuestieDB
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
QuestieSearch.types = {"npc", "object", "item", "quest"}
-- Save search results, so the next search has a smaller set to search
QuestieSearch.LastResult = {
query = '',
queryType = '',
quest = {},
npc = {},
object = {},
item = {},
}
local function _ResetResults()
QuestieSearch.LastResult = {
query = '',
queryType = '',
quest = {},
npc = {},
object = {},
item = {},
}
end
-- Execute a search by name for all types
function QuestieSearch:ByName(query)
_ResetResults()
for _, type in pairs(QuestieSearch.types) do
QuestieSearch:Search(query, type, "chars")
end
return QuestieSearch.LastResult
end
-- Execute a search by ID for all types
function QuestieSearch:ByID(query)
_ResetResults()
for _,type in pairs(QuestieSearch.types) do
QuestieSearch:Search(query, type, "int")
end
return QuestieSearch.LastResult
end
--[[
QuestieSearch:Search
This function searches a value from the database, including partial matches.
Adds the values to QuestieSearch.LastResult.
Returns table of found IDs for the selected search type.
Parameters:
query The search string/int
searchType Which database to search, possible values:
"npc"
"object"
"item"
"quest"
queryType Which type of search to run, possible values:
"chars"
"int"
Optional. Default: "chars"
--]]
function QuestieSearch:Search(rawQuery, searchType, queryType)
queryType = queryType or "chars"
local databaseQueryHandle
local databaseKeys
local overrideKeys
local ascensionKeys
if searchType == "npc" then
databaseQueryHandle = QuestieDB.QueryNPCSingle
databaseKeys = QuestieDB.NPCPointers
overrideKeys = QuestieDB.npcDataOverrides
ascensionKeys = QuestieDB.ascensionNpcIds
elseif searchType == "object" then
databaseQueryHandle = QuestieDB.QueryObjectSingle
databaseKeys = QuestieDB.ObjectPointers
overrideKeys = QuestieDB.objectDataOverrides
ascensionKeys = QuestieDB.ascensionObjectIds
elseif searchType == "item" then
databaseQueryHandle = QuestieDB.QueryItemSingle
databaseKeys = QuestieDB.ItemPointers
overrideKeys = QuestieDB.itemDataOverrides
ascensionKeys = QuestieDB.ascensionItemIds
elseif searchType == "quest" then
databaseQueryHandle = QuestieDB.QueryQuestSingle
databaseKeys = QuestieDB.QuestPointers
overrideKeys = QuestieDB.questDataOverrides
ascensionKeys = QuestieDB.ascensionQuestIds
else
return
end
local sanitizedQuery
local strictSearch = false
if type(rawQuery) ~= "number" then
local stringFirst, stringLast = rawQuery:sub(1, 1), rawQuery:sub(-1)
if (stringFirst == '"' or stringFirst == "'") and (stringLast == stringFirst) then
strictSearch = true
sanitizedQuery = rawQuery:sub(2, -2)
else
sanitizedQuery = rawQuery
end
else
sanitizedQuery = rawQuery
end
local searchCount = 0;
local isTextSearch = queryType == "chars"
local isIdSearch = queryType == "int" and tonumber(sanitizedQuery) ~= nil
local lastResults = QuestieSearch.LastResult[searchType]
-- Fast-path for ID search: allow direct lookups even when the ID isn't in the compiled pointer tables
-- (e.g. Ascension custom entries injected via *Overrides*).
if isIdSearch then
local directId = tonumber(sanitizedQuery)
if directId then
local directName = databaseQueryHandle(directId, "name")
if directName then
lastResults[directId] = true
QuestieSearch.LastResult.query = rawQuery
return lastResults
end
end
end
if isTextSearch then
local queryToFind = string.lower(sanitizedQuery)
local visited = {}
local function _RunNameMatch(id)
local name = databaseQueryHandle(id, "name") -- Some entries don't have a 'name' because of the way we load corrections
if strictSearch then
if name and (string.lower(name) == queryToFind) then -- strict search
searchCount = searchCount + 1;
QuestieSearch.LastResult[searchType][id] = true;
else
QuestieSearch.LastResult[searchType][id] = nil;
end
else
if name and string.find(string.lower(name), queryToFind) then -- fuzzy search
searchCount = searchCount + 1;
QuestieSearch.LastResult[searchType][id] = true;
else
QuestieSearch.LastResult[searchType][id] = nil;
end
end
end
if type(databaseKeys) == "table" then
for id, _ in pairs(databaseKeys) do
visited[id] = true
_RunNameMatch(id)
end
end
-- Include custom data injected via overrides, since these IDs may not exist in *Pointers.
if type(overrideKeys) == "table" then
for id, _ in pairs(overrideKeys) do
if not visited[id] then
_RunNameMatch(id)
end
end
end
-- Also include Ascension custom IDs
if type(ascensionKeys) == "table" then
for id, _ in pairs(ascensionKeys) do
if not visited[id] then
visited[id] = true
_RunNameMatch(id)
end
end
end
elseif isIdSearch then
-- Fast-path for custom data injected via overrides (may not exist in *Pointers)
local qid = tonumber(sanitizedQuery)
if qid and databaseQueryHandle(qid, "name") then
lastResults[qid] = true
searchCount = searchCount + 1
end
for id, _ in pairs(databaseKeys) do
if strictSearch then
if tostring(id) == sanitizedQuery then -- strict search
-- We have a search result or a favourite to display
searchCount = searchCount + 1;
lastResults[id] = true;
else
-- This entry doesn't meet the search criteria, removed from the last results
lastResults[id] = nil;
end
else
if string.find(tostring(id), sanitizedQuery) then -- fuzzy search
-- We have a search result or a favourite to display
searchCount = searchCount + 1;
lastResults[id] = true;
else
-- This entry doesn't meet the search criteria, removed from the last results
lastResults[id] = nil;
end
end
end
end
QuestieSearch.LastResult.query = rawQuery
return QuestieSearch.LastResult[searchType]
end
+872
View File
@@ -0,0 +1,872 @@
---@class QuestieSearchResults
local QuestieSearchResults = QuestieLoader:CreateModule("QuestieSearchResults")
-------------------------
--Import modules.
-------------------------
---@type QuestieQuest
local QuestieQuest = QuestieLoader:ImportModule("QuestieQuest")
---@type QuestieJourney
local QuestieJourney = QuestieLoader:ImportModule("QuestieJourney")
local _QuestieJourney = QuestieJourney.private
---@type QuestieJourneyUtils
local QuestieJourneyUtils = QuestieLoader:ImportModule("QuestieJourneyUtils")
---@type QuestieSearch
local QuestieSearch = QuestieLoader:ImportModule("QuestieSearch")
---@type QuestieMap
local QuestieMap = QuestieLoader:ImportModule("QuestieMap")
---@type QuestieDB
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
---@type QuestieCorrections
local QuestieCorrections = QuestieLoader:ImportModule("QuestieCorrections")
---@type QuestieLib
local QuestieLib = QuestieLoader:ImportModule("QuestieLib")
---@type QuestieLink
local QuestieLink = QuestieLoader:ImportModule("QuestieLink")
---@type l10n
local l10n = QuestieLoader:ImportModule("l10n")
local stringrep = string.rep
local stringsub = string.sub
local AceGUI = LibStub("AceGUI-3.0");
local _HandleOnGroupSelected
local lastOpenSearch = "quest"
local _selected = 0
local BY_NAME = 1
local BY_ID = 2
local function AddParagraph(frame, lookupObject, secondKey, header, query)
if lookupObject[secondKey] then
QuestieJourneyUtils:AddLine(frame, Questie:Colorize(header, "yellow"))
for _,id in pairs(lookupObject[secondKey]) do
local name = query(id, "name")
if name then
QuestieJourneyUtils:AddLine(frame, name.." ("..id..")")
end
end
end
end
---Takes a frame and adds a paragraph with a header text and a list of links to other search results
---@param frame AceGUIWidget The frame to work on
---@param linkType string The type of result to link to (npc|object|quest|item)
---@param lookupObject table Table of IDs (npc|object|quest|item)
---@param header string The text header to show above the links
---@param query function The function used to get link name from
local function AddLinkedParagraph(frame, linkType, lookupObject, header, query)
if lookupObject and #lookupObject > 0 then
QuestieJourneyUtils:AddLine(frame, Questie:Colorize(header, "yellow"))
for _,id in pairs(lookupObject) do
-- QuestieJourneyUtils:AddLine(frame, lookupDB[id][lookupKey].." ("..id..")")
local link = AceGUI:Create("InteractiveLabel")
link:SetText(query(id, "name").." ("..id..")");
link:SetCallback("OnClick", function() QuestieSearchResults:SetSearch(linkType, id) end)
frame:AddChild(link);
end
end
end
-- Create a button for showing/hiding manual notes of NPCs/objects
local function CreateShowHideButton(id)
-- Initialise button
local button = AceGUI:Create("Button")
button.id = id
if (not QuestieMap.manualFrames["any"]) or (not QuestieMap.manualFrames["any"][id]) then
button:SetText(l10n("Show on Map"))
button:SetCallback("OnClick", function(self) self:ShowOnMap(self) end)
else
button:SetText(l10n("Remove from Map"))
button:SetCallback("OnClick", function(self) self:RemoveFromMap(self) end)
end
-- Functions for showing/hiding and switching behaviour afterwards
button.RemoveFromMap = function(self)
if self.idsToShow then
for _, spawnId in pairs(self.idsToShow) do
QuestieMap:UnloadManualFrames(spawnId)
end
else
QuestieMap:UnloadManualFrames(self.id)
end
self:SetText(l10n("Show on Map"))
self:SetCallback("OnClick", function() self:ShowOnMap(self) end)
end
button.ShowOnMap = function(self)
if self.idsToShow then
for _, spawnId in pairs(self.idsToShow) do
if spawnId > 0 then
QuestieMap:ShowNPC(spawnId)
else
QuestieMap:ShowObject(-spawnId)
end
end
else
if self.id > 0 then
QuestieMap:ShowNPC(self.id)
elseif self.id < 0 then
QuestieMap:ShowObject(-self.id)
end
end
self:SetText(l10n("Remove from Map"))
self:SetCallback("OnClick", function() self:RemoveFromMap(self) end)
end
return button
end
local function rec(theTable, ret, indent)
ret = ret..stringrep(' ', indent)..'{\n'
indent = indent + 1
for k, v in pairs(theTable) do
local t = type(v)
if t == 'nil' then
ret = ret..stringrep(' ', indent)..'['..k..']=nil'
elseif t == 'table' then
ret = rec(v, ret..stringrep(' ', indent)..'['..k..']=\n', indent)
else
ret = ret..stringrep(' ', indent)..'['..k..']='..v
end
ret = ret..'\n'
end
return ret..stringrep(' ', indent-1)..'},'
end
local function recurseTable(theTable, theKeys)
local ret = Questie:Colorize('Raw data (shown because debug is enabled):\n\n', 'red')
for key, _ in pairs(theKeys) do
ret = ret..Questie:Colorize(key, 'yellow')..': '
local t = type(theTable[key])
if t == 'nil' then
ret = ret..'nil'
elseif t == 'table' then
ret = rec(theTable[key], ret, 0)
else
ret = ret..theTable[key]
end
ret = ret..'\n'
end
return ret
end
function QuestieSearchResults:QuestDetailsFrame(details, id)
local ret = QuestieDB.QueryQuest(id, {"name", "requiredLevel", "requiredRaces", "objectivesText", "startedBy", "finishedBy", "preQuestGroup", "preQuestSingle"}) or {}
local name, requiredLevel, requiredRaces, objectivesText, startedBy, finishedBy, preQuestGroup, preQuestSingle = ret[1], ret[2], ret[3], ret[4], ret[5], ret[6], ret[7], ret[8]
local questLevel, _ = QuestieLib.GetTbcLevel(id);
-- header
local title = AceGUI:Create("Heading")
title:SetFullWidth(true);
title:SetText(name)
details:AddChild(title)
-- is quest finished by player
local finished = AceGUI:Create("CheckBox")
finished:SetValue(Questie.db.char.complete[id])
finished:SetLabel(l10n("Complete"))
finished:SetDisabled(true)
-- reduce offset to next checkbox
finished:SetHeight(16)
details:AddChild(finished)
-- hidden by user
local hiddenByUser = AceGUI:Create("CheckBox")
hiddenByUser.id = id
hiddenByUser:SetLabel(l10n("Hidden"))
if Questie.db.char.hidden[id] ~= nil then
hiddenByUser:SetValue(true)
else
hiddenByUser:SetValue(false)
end
hiddenByUser:SetCallback("OnValueChanged", function(frame)
if Questie.db.char.hidden[frame.id] ~= nil then
frame:SetValue(false)
QuestieQuest:UnhideQuest(frame.id)
else
frame:SetValue(true)
QuestieQuest:HideQuest(frame.id)
end
end)
hiddenByUser:SetCallback("OnEnter", function()
if GameTooltip:IsShown() then
return;
end
GameTooltip:SetOwner(_G["QuestieJourneyFrame"].frame:GetParent(), "ANCHOR_CURSOR");
GameTooltip:AddLine(l10n("Quest is hidden"))
GameTooltip:AddLine(l10n("\nWhen selected, hides the quest from the map, even if it is active.\n\nHiding a quest is also possible by Shift-clicking it on the map."), 1, 1, 1, true);
GameTooltip:SetFrameStrata("TOOLTIP");
GameTooltip:Show();
end)
hiddenByUser:SetCallback("OnLeave", function()
if GameTooltip:IsShown() then
GameTooltip:Hide();
end
end)
-- reduce offset to next checkbox
hiddenByUser:SetHeight(16)
details:AddChild(hiddenByUser)
-- hidden by Questie
local hiddenQuests = AceGUI:Create("CheckBox")
hiddenQuests:SetValue(QuestieCorrections.hiddenQuests[id])
hiddenQuests:SetLabel(l10n("Hidden by Questie"))
hiddenQuests:SetDisabled(true)
-- do not reduce offset, as checkbox is followed by text
details:AddChild(hiddenQuests)
-- general info
QuestieJourneyUtils:AddLine(details, Questie:Colorize(l10n("Quest ID"), "yellow") .. ": " .. id)
QuestieJourneyUtils:AddLine(details, Questie:Colorize(l10n("Quest Level"), "yellow") .. ": " .. questLevel)
QuestieJourneyUtils:AddLine(details, Questie:Colorize(l10n("Required Level"), "yellow") .. ": " .. requiredLevel)
local reqRaces = QuestieLib:GetRaceString(requiredRaces)
if (reqRaces ~= "") then
QuestieJourneyUtils:AddLine(details, Questie:Colorize(l10n("Required Race"), "yellow") .. ": " .. reqRaces)
end
QuestieJourneyUtils:AddLine(details, Questie:Colorize(l10n("Doable"), "yellow") .. ": " .. tostring(QuestieDB.IsDoableVerbose(id, false, true, true)))
-- objectives text
if objectivesText then
QuestieJourneyUtils:AddLine(details, "")
QuestieJourneyUtils:AddLine(details, Questie:Colorize(l10n("Objectives"), "yellow") .. ":")
for _, v in pairs(objectivesText) do
QuestieJourneyUtils:AddLine(details, v)
end
end
if startedBy then
-- quest starters
QuestieJourneyUtils:AddLine(details, "")
AddLinkedParagraph(details, "npc", startedBy[1], l10n("NPCs starting this quest:"), QuestieDB.QueryNPCSingle)
AddLinkedParagraph(details, "object", startedBy[2], l10n("Objects starting this quest:"), QuestieDB.QueryObjectSingle)
-- TODO change to linked paragraph once item details page exists
AddParagraph(details, startedBy, 3, l10n("Items starting this quest:"), QuestieDB.QueryItemSingle)
end
if finishedBy then
-- quest finishers
QuestieJourneyUtils:AddLine(details, "")
AddLinkedParagraph(details, "npc", finishedBy[1], l10n("NPCs finishing this quest:"), QuestieDB.QueryNPCSingle)
AddLinkedParagraph(details, "object", finishedBy[2], l10n("Objects finishing this quest:"), QuestieDB.QueryObjectSingle)
end
-- pre quests
if preQuestGroup then
QuestieJourneyUtils:AddLine(details, "")
AddLinkedParagraph(details, "quest", preQuestGroup, l10n("Requires all of these quests to be finished:"), QuestieDB.QueryQuestSingle)
end
if preQuestSingle then
QuestieJourneyUtils:AddLine(details, "")
AddLinkedParagraph(details, "quest", preQuestSingle, l10n("Requires one of these quests to be finished:"), QuestieDB.QueryQuestSingle)
end
QuestieJourneyUtils:AddLine(details, "")
if Questie.db.profile.debugEnabled then
QuestieJourneyUtils:AddLine(details, recurseTable(QuestieDB.GetQuest(id), QuestieDB.questKeys))
end
end
function QuestieSearchResults:SpawnDetailsFrame(f, spawn, spawnType)
local header = AceGUI:Create("Heading");
header:SetFullWidth(true);
local id = 0
local typeLabel = ""
local query
if spawnType == "npc" then
id = spawn
typeLabel = "NPC"
query = QuestieDB.QueryNPCSingle
elseif spawnType == "object" then
id = -spawn
typeLabel = "Object"
query = QuestieDB.QueryObjectSingle
end
header:SetText(query(spawn, "name"));
f:AddChild(header);
QuestieJourneyUtils:Spacer(f);
local spawnID = AceGUI:Create("Label");
spawnID:SetText(typeLabel.." ID: "..spawn);
spawnID:SetFullWidth(true);
f:AddChild(spawnID);
QuestieJourneyUtils:Spacer(f);
local spawnZone = AceGUI:Create("Label");
local spawns = query(spawn, "spawns")
if spawns then
f:AddChild(CreateShowHideButton(id))
local startindex = 0;
for i in pairs(spawns) do
if spawns[i][1] then
startindex = i;
break;
end
end
local zoneName = QuestieJourneyUtils:GetZoneName(startindex)
spawnZone:SetText(l10n(zoneName));
spawnZone:SetFullWidth(true);
f:AddChild(spawnZone);
if spawns[startindex] and spawns[startindex][1] then
local startx = spawns[startindex][1][1];
local starty = spawns[startindex][1][2];
if (startx ~= -1 or starty ~= -1) then
local spawnLoc = AceGUI:Create("Label");
spawnLoc:SetText("X: ".. startx .." || Y: ".. starty);
spawnLoc:SetFullWidth(true);
f:AddChild(spawnLoc);
end
end
else
spawnZone:SetText(l10n("No spawn data available."))
spawnZone:SetFullWidth(true);
f:AddChild(spawnZone);
end
-- Also Starts
local questStarts = query(spawn, "questStarts")
if questStarts then
local startGroup = AceGUI:Create("InlineGroup");
startGroup:SetFullWidth(true);
startGroup:SetLayout("flow");
startGroup:SetTitle(l10n("Starts the following quests:"));
f:AddChild(startGroup);
local startQuests = {};
local counter = 1;
for _, v in pairs(questStarts) do
local quest = QuestieDB.GetQuest(v)
local frame = AceGUI:Create("InteractiveLabel")
frame:SetUserData("id", v)
frame:SetUserData("name", quest.name)
frame:SetCallback("OnClick", function() QuestieSearchResults:SetSearch("quest", v) end)
frame:SetCallback("OnEnter", _QuestieJourney.ShowJourneyTooltip)
frame:SetCallback("OnLeave", _QuestieJourney.HideJourneyTooltip)
frame:SetText(QuestieLib:GetColoredQuestName(quest.Id, true, true))
startQuests[counter] = {
frame = frame,
quest = quest
}
startGroup:AddChild(frame)
counter = counter + 1
end
if #startQuests == 0 then
local noquest = AceGUI:Create("Label");
noquest:SetText(l10n("No quests to list."));
noquest:SetFullWidth(true);
startGroup:AddChild(noquest);
end
end
QuestieJourneyUtils:Spacer(f);
-- Also ends
local questEnds = query(spawn, "questEnds")
if questEnds then
local endGroup = AceGUI:Create("InlineGroup");
endGroup:SetFullWidth(true);
endGroup:SetLayout("flow");
endGroup:SetTitle(l10n("Ends the following quests:"));
f:AddChild(endGroup);
local endQuests = {};
local counter = 1;
for _, v in ipairs(questEnds) do
local quest = QuestieDB.GetQuest(v)
local frame = AceGUI:Create("InteractiveLabel")
frame:SetText(QuestieLib:GetColoredQuestName(quest.Id, true, true))
frame:SetUserData("id", v)
frame:SetUserData("name", quest.name)
frame:SetCallback("OnClick", function() QuestieSearchResults:SetSearch("quest", v) end)
frame:SetCallback("OnEnter", _QuestieJourney.ShowJourneyTooltip)
frame:SetCallback("OnLeave", _QuestieJourney.HideJourneyTooltip)
endQuests[counter] = {
frame = frame,
quest = quest
}
endGroup:AddChild(frame)
counter = counter + 1
end
if #endQuests == 0 then
local noquest = AceGUI:Create("Label");
noquest:SetText(l10n("No quests to list."));
noquest:SetFullWidth(true);
endGroup:AddChild(noquest);
end
end
QuestieJourneyUtils:Spacer(f);
if Questie.db.profile.debugEnabled then
if spawnType == "npc" then
QuestieJourneyUtils:AddLine(f, recurseTable(QuestieDB:GetNPC(spawn), QuestieDB.npcKeys))
elseif spawnType == "object" then
QuestieJourneyUtils:AddLine(f, recurseTable(QuestieDB:GetObject(spawn), QuestieDB.objectKeys))
end
end
-- Fix for sometimes the scroll content will max out and not show everything until window is resized
f.content:SetHeight(10000);
end
function QuestieSearchResults:ItemDetailsFrame(f, itemId)
local header = AceGUI:Create("Heading")
header:SetFullWidth(true)
local query = QuestieDB.QueryItemSingle
header:SetText(query(itemId, "name"))
f:AddChild(header)
local itemLink = select(2, GetItemInfo(itemId))
local itemIcon = AceGUI:Create("Icon")
itemIcon:SetWidth(25)
itemIcon:SetHeight(25)
itemIcon:SetImage(GetItemIcon(itemId))
itemIcon:SetImageSize(25, 25)
itemIcon:SetCallback("OnEnter", function()
if (not itemLink) then
itemLink = select(2, GetItemInfo(itemId))
end
GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
if itemLink then
GameTooltip:SetHyperlink(itemLink)
elseif QuestieCompat.Is335 then
-- I don't know if this applies to private servers, but let's assume it does.
GameTooltip:AddLine("Item Unavailable", 1, 0, 0)
GameTooltip:AddLine("This item is unsafe. To view this item without the risk of disconnection, you need to have first seen it in the game world. This is a restriction enforced by Blizzard since Patch 1.10.", nil, nil, nil, 1)
GameTooltip:AddLine(" ");
GameTooltip:AddLine("You can |cffFFFFFFLEFT-CLICK|r to attempt to query the server. You may be disconnected.", .75, .75, .75, 1)
itemIcon:SetCallback("OnClick", function()
GameTooltip:SetHyperlink("item:"..itemId..":0:0:0:0:0:0:0")
end)
end
GameTooltip:Show()
end)
itemIcon:SetCallback("OnLeave", function()
GameTooltip:Hide()
end)
f:AddChild(itemIcon)
local spawnIdLabel = AceGUI:Create("Label")
spawnIdLabel:SetText(" Item ID: " .. itemId)
f:AddChild(spawnIdLabel)
if QuestieCorrections.questItemBlacklist[itemId] then
QuestieJourneyUtils:Spacer(f)
local itemBlacklistedLabel = AceGUI:Create("Label")
itemBlacklistedLabel:SetText(l10n("This item is blacklisted because it has too many sources"))
itemBlacklistedLabel:SetFullWidth(true)
f:AddChild(itemBlacklistedLabel)
return
end
local sources = QuestieDB.QueryItem(itemId, {"npcDrops", "objectDrops", "vendors"})
local npcDrops = sources[1]
local objectDrops = sources[2]
local vendors = sources[3]
local npcSpawnsHeading = AceGUI:Create("Heading")
npcSpawnsHeading:SetText(l10n("NPCs"))
npcSpawnsHeading:SetFullWidth(true)
f:AddChild(npcSpawnsHeading)
if (not npcDrops or not next(npcDrops)) then
local noNPCSourcesLabel = AceGUI:Create("Label")
noNPCSourcesLabel:SetText(l10n("No NPC drops this item"))
noNPCSourcesLabel:SetFullWidth(true)
f:AddChild(noNPCSourcesLabel)
else
local npcLabel = AceGUI:Create("Label")
local npcIdsWithSpawns = {}
for _, npcId in pairs(npcDrops) do
local spawns = QuestieDB.QueryNPCSingle(npcId, "spawns")
if spawns then
npcIdsWithSpawns[#npcIdsWithSpawns + 1] = npcId
end
end
npcLabel:SetText(l10n("%d NPCs drop this item", #npcIdsWithSpawns))
f:AddChild(npcLabel)
if (#npcIdsWithSpawns > 0) then
local showHideButton = CreateShowHideButton(itemId)
showHideButton.idsToShow = npcIdsWithSpawns
f:AddChild(showHideButton)
end
end
local objectSpawnsHeading = AceGUI:Create("Heading")
objectSpawnsHeading:SetText(l10n("Objects"))
objectSpawnsHeading:SetFullWidth(true)
f:AddChild(objectSpawnsHeading)
if (not objectDrops or not next(objectDrops)) then
local noObjectSourcesLabel = AceGUI:Create("Label")
noObjectSourcesLabel:SetText(l10n("No Object drops this item"))
noObjectSourcesLabel:SetFullWidth(true)
f:AddChild(noObjectSourcesLabel)
else
local objectLabel = AceGUI:Create("Label")
local objectIdsWithSpawns = {}
for _, objectId in pairs(objectDrops) do
local spawns = QuestieDB.QueryObjectSingle(objectId, "spawns")
if spawns then
objectIdsWithSpawns[#objectIdsWithSpawns + 1] = -objectId
end
end
objectLabel:SetText(l10n("%d Objects drop this item", #objectIdsWithSpawns))
f:AddChild(objectLabel)
if (#objectIdsWithSpawns > 0) then
local showHideButton = CreateShowHideButton(itemId)
showHideButton.idsToShow = objectIdsWithSpawns
f:AddChild(showHideButton)
end
end
local vendorSpawnsHeading = AceGUI:Create("Heading")
vendorSpawnsHeading:SetText(l10n("Vendors"))
vendorSpawnsHeading:SetFullWidth(true)
f:AddChild(vendorSpawnsHeading)
if (not vendors or not next(vendors)) then
local noVendorSourcesLabel = AceGUI:Create("Label")
noVendorSourcesLabel:SetText(l10n("No Vendor sells this item"))
noVendorSourcesLabel:SetFullWidth(true)
f:AddChild(noVendorSourcesLabel)
else
local vendorLabel = AceGUI:Create("Label")
local vendorIdsWithSpawns = {}
for _, npcId in pairs(vendors) do
local spawns = QuestieDB.QueryNPCSingle(npcId, "spawns")
if spawns then
vendorIdsWithSpawns[#vendorIdsWithSpawns + 1] = npcId
end
end
vendorLabel:SetText(l10n("%d Vendors sell this item", #vendorIdsWithSpawns))
f:AddChild(vendorLabel)
if (#vendorIdsWithSpawns > 0) then
local showHideButton = CreateShowHideButton(itemId)
showHideButton.idsToShow = vendorIdsWithSpawns
f:AddChild(showHideButton)
end
end
if Questie.db.profile.debugEnabled then
QuestieJourneyUtils:AddLine(f, recurseTable(QuestieDB:GetItem(itemId), QuestieDB.itemKeys))
end
-- Fix for sometimes the scroll content will max out and not show everything until window is resized
f.content:SetHeight(10000);
end
-- draws a list of results of a certain type, e.g. "quest"
function QuestieSearchResults:DrawResultTab(container, resultType)
-- probably already done by `JourneySelectTabGroup`, doesn't hurt to be safe though
container:ReleaseChildren();
local results = {}
local database
if resultType == "quest" then
database = QuestieDB.QueryQuestSingle
elseif resultType == "npc" then
database = QuestieDB.QueryNPCSingle
elseif resultType == "object" then
database = QuestieDB.QueryObjectSingle
elseif resultType == "item" then
database = QuestieDB.QueryItemSingle
else
return
end
for k,_ in pairs(QuestieSearch.LastResult[resultType]) do
local name = database(k, "name")
if name then
local complete = ''
if Questie.db.char.complete[k] and resultType == "quest" then
complete = Questie:Colorize("(" .. l10n("Complete") .. ")" , "green")
end
-- TODO rename option to "enabledIDs" or create separate ones for npcs/objects/items
local id = ''
if Questie.db.profile.enableTooltipsQuestID then
id = ' (' .. k .. ')'
end
table.insert(results, {
["text"] = complete .. name .. id,
["value"] = tonumber(k)
})
end
end
local resultFrame = AceGUI:Create("SimpleGroup");
resultFrame:SetLayout("Fill");
resultFrame:SetFullWidth(true);
resultFrame:SetFullHeight(true);
local resultTree = AceGUI:Create("TreeGroup");
resultTree:SetFullWidth(true);
resultTree:SetFullHeight(true);
resultTree.treeframe:SetWidth(260);
resultTree:SetTree(results);
resultTree:SetCallback("OnGroupSelected", _HandleOnGroupSelected)
resultFrame:AddChild(resultTree)
container:AddChild(resultFrame);
if _selected ~= 0 then
resultTree:SelectByValue(_selected)
_selected = 0
end
end
_HandleOnGroupSelected = function (resultType)
-- This is either the questId, npcId, objectId or itemId
local selectedId = tonumber(resultType.localstatus.selected)
if IsShiftKeyDown() and lastOpenSearch == "quest" then
local questName = QuestieDB.QueryQuestSingle(selectedId, "name")
local questLevel, _ = QuestieLib.GetTbcLevel(selectedId);
ChatEdit_InsertLink(QuestieLink:GetQuestLinkString(questLevel, questName, selectedId))
end
-- get master frame and create scroll frame inside
local master = resultType.frame.obj;
master:ReleaseChildren();
master:SetLayout("Fill");
master:SetFullWidth(true);
master:SetFullHeight(true);
local details = AceGUI:Create("ScrollFrame");
details:SetLayout("Flow");
master:AddChild(details);
if lastOpenSearch == "quest" then
QuestieSearchResults:QuestDetailsFrame(details, selectedId);
elseif lastOpenSearch == "npc" then
QuestieSearchResults:SpawnDetailsFrame(details, selectedId, 'npc');
elseif lastOpenSearch == "object" then
QuestieSearchResults:SpawnDetailsFrame(details, selectedId, 'object')
elseif lastOpenSearch == "item" then
QuestieSearchResults:ItemDetailsFrame(details, selectedId)
end
end
local function SelectTabGroup(container, _, resultType)
lastOpenSearch = resultType
QuestieSearchResults:DrawResultTab(container, resultType);
end
local function _GetSearchFunction(searchBox, searchGroup)
return function()
if searchBox:GetText() ~= "" then
local searchText = searchBox:GetText()
local itemName = GetItemInfo(searchText)
if stringsub(searchText, 1, 4) == "|cff" and itemName then
-- An itemLink was added to the searchBox
searchBox:SetText(itemName)
QuestieSearchResults:DrawSearchResultTab(searchGroup, Questie.db.profile.searchType, itemName, false)
elseif stringsub(searchText, 1, 4) == "|cff" then
-- This should be impossible to reach, since when you see an item link in the game the item should
-- be cached already which would be caught by the condition above
Questie:Debug(Questie.DEBUG_DEVELOP, "Search with link of an uncached item")
else
-- Normal search
local text = string.trim(searchText, " \n\r\t[]");
QuestieSearchResults:DrawSearchResultTab(searchGroup, Questie.db.profile.searchType, text, false)
end
searchBox:ClearFocus()
end
end
end
-- Draw search results from advanced search tab
local searchResultTabs
function QuestieSearchResults:DrawSearchResultTab(searchGroup, searchType, query, useLast)
if not searchResultTabs then
searchGroup:ReleaseChildren();
if searchType == BY_NAME and (not useLast) then
QuestieSearch:ByName(query)
elseif searchType == BY_ID and (not useLast) then
QuestieSearch:ByID(query)
end
local results = QuestieSearch.LastResult;
local resultTypes = {
["quest"] = "Quests",
["npc"] = "Mobs",
["object"] = "Objects",
["item"] = "Items",
}
local resultCountTotal = 0
local resultCounts = {
total = 0,
quest = 0,
npc = 0,
object = 0,
item = 0
}
for type,_ in pairs(resultTypes) do
for _,_ in pairs(results[type]) do
resultCountTotal = resultCountTotal + 1
resultCounts[type] = resultCounts[type] + 1
end
end
if (resultCountTotal == 0) then
local noresults = AceGUI:Create("Label");
noresults:SetText(Questie:Colorize(l10n('No Match for Search Results: %s', query), 'yellow'));
noresults:SetFullWidth(true);
searchGroup:AddChild(noresults);
return;
end
searchResultTabs = AceGUI:Create("TabGroup");
searchResultTabs:SetFullWidth(true);
searchResultTabs:SetFullHeight(true);
searchResultTabs:SetLayout("Flow");
searchResultTabs:SetTabs({
{
text = l10n('Quests') .. " ("..resultCounts.quest..")",
value = "quest",
disabled = resultCounts.quest == 0,
},
{
text = l10n('NPCs') .. " ("..resultCounts.npc..")",
value = "npc",
disabled = resultCounts.npc == 0,
},
{
text = l10n('Objects') .. " ("..resultCounts.object..")",
value = "object",
disabled = resultCounts.object == 0,
},
{
text = l10n('Items') .. " ("..resultCounts.item..")",
value = "item",
disabled = resultCounts.item == 0,
},
})
searchResultTabs:SetCallback("OnGroupSelected", SelectTabGroup)
if _selected == 0 then searchResultTabs:SelectTab("quest"); end
searchGroup:AddChild(searchResultTabs);
else
searchGroup:ReleaseChildren();
searchResultTabs = nil;
self:DrawSearchResultTab(searchGroup, searchType, query, useLast);
end
end
-- The "Advanced Search" tab
local typeDropdown
local searchBox
local searchGroup
local searchButton
function QuestieSearchResults:DrawSearchTab(container)
-- Header
local header = AceGUI:Create("Heading");
header:SetText(l10n("Enter in your Search"));
header:SetFullWidth(true);
container:AddChild(header);
QuestieJourneyUtils:Spacer(container);
searchBox = AceGUI:Create("EditBox");
searchGroup = AceGUI:Create("SimpleGroup");
searchButton = AceGUI:Create("Button");
typeDropdown = AceGUI:Create("Dropdown");
typeDropdown:SetList({
[1] = l10n("Search By Name"),
[2] = l10n("Search By ID"),
});
typeDropdown:SetValue(Questie.db.profile.searchType);
typeDropdown:SetCallback("OnValueChanged", function(key, _)
Questie.db.profile.searchType = key.value;
searchGroup:ReleaseChildren();
searchBox:HighlightText();
searchBox:SetFocus();
end)
container:AddChild(typeDropdown);
searchBox:SetFocus();
searchBox:SetRelativeWidth(0.6);
searchBox:SetLabel(l10n("Advanced Search") .. " (".. l10n("Quests") .. ", ".. l10n("NPCs") .. ", ".. l10n("Objects") .. ", ".. l10n("Items") .. ")");
searchBox:DisableButton(true);
searchBox:SetCallback("OnTextChanged", function()
if searchBox:GetText() ~= "" then
searchButton:SetDisabled(false);
else
searchButton:SetDisabled(true);
end
end);
searchBox:SetCallback("OnEnterPressed", _GetSearchFunction(searchBox, searchGroup));
-- Check for existence of previous search, if present use its text
if QuestieSearch.LastResult.query ~= "" then
searchBox:SetText(QuestieSearch.LastResult.query)
end
container:AddChild(searchBox);
searchButton:SetText(l10n("Search"));
searchButton:SetDisabled(true);
searchButton:SetCallback("OnClick", _GetSearchFunction(searchBox, searchGroup));
container:AddChild(searchButton);
searchGroup:SetFullHeight(true);
searchGroup:SetFullWidth(true);
searchGroup:SetLayout("fill");
container:AddChild(searchGroup);
-- Check for existence of previous search, if present use its result
if QuestieSearch.LastResult.query ~= "" then
searchButton:SetDisabled(false)
local text = string.trim(searchBox:GetText(), " \n\r\t[]")
QuestieSearchResults:DrawSearchResultTab(searchGroup, Questie.db.profile.searchType, text, true)
end
end
function QuestieSearchResults:GetDetailFrame(detailType, id)
local frame = AceGUI:Create("Frame")
frame:SetHeight(500)
frame:SetWidth(300)
frame:SetLayout("Fill");
local details = AceGUI:Create("ScrollFrame")
details:SetFullWidth(true);
details:SetFullHeight(true);
details:SetLayout("Flow")
frame:AddChild(details)
if detailType == "quest" then
QuestieSearchResults:QuestDetailsFrame(details, id)
frame:SetTitle(l10n("Quest Details"))
elseif detailType == "npc" then
QuestieSearchResults:SpawnDetailsFrame(details, id, detailType)
frame:SetTitle(l10n("NPC Details"))
elseif detailType == "object" then
QuestieSearchResults:SpawnDetailsFrame(details, id, detailType)
frame:SetTitle(l10n("Object Details"))
elseif detailType == "item" then
QuestieSearchResults:ItemDetailsFrame(details, id)
frame:SetTitle(l10n("Item Details"))
else
frame:ReleaseChildren()
return
end
frame:Show()
end
function QuestieSearchResults:SetSearch(detailType, id)
_selected = id
searchBox:SetText(tostring(id))
Questie.db.profile.searchType = BY_ID
typeDropdown:SetValue(BY_ID)
QuestieSearchResults:DrawSearchResultTab(searchGroup, BY_ID, id, false)
searchResultTabs:SelectTab(detailType)
end
@@ -0,0 +1,156 @@
---@type QuestieJourney
local QuestieJourney = QuestieLoader:CreateModule("QuestieJourney")
local _QuestieJourney = QuestieJourney.private
_QuestieJourney.myJourney = {}
_QuestieJourney.notePopup = nil
---@type QuestieJourneyUtils
local QuestieJourneyUtils = QuestieLoader:ImportModule("QuestieJourneyUtils")
---@type QuestieDB
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
---@type l10n
local l10n = QuestieLoader:ImportModule("l10n")
--- COMPATIBILITY ---
local CALENDAR_WEEKDAY_NAMES = QuestieCompat.CALENDAR_WEEKDAY_NAMES
local CALENDAR_FULLDATE_MONTH_NAMES = QuestieCompat.CALENDAR_FULLDATE_MONTH_NAMES
local AceGUI = LibStub("AceGUI-3.0");
local journeyTreeFrame
-- manage the journey tree
function _QuestieJourney.myJourney:ManageTree(container)
if not journeyTreeFrame then
journeyTreeFrame = AceGUI:Create("TreeGroup");
journeyTreeFrame:SetFullWidth(true);
journeyTreeFrame:SetFullHeight(true);
journeyTreeFrame.treeframe:SetWidth(220);
local journeyTree = _QuestieJourney:GetHistory();
journeyTreeFrame:SetTree(journeyTree);
local latestMonth, latestYear = _QuestieJourney:GetMonthAndYearOfLatestEntry()
if latestMonth and latestYear then
journeyTreeFrame:SelectByPath(latestYear, latestMonth)
end
journeyTreeFrame:SetCallback("OnGroupSelected", function(group)
Questie:Debug(Questie.DEBUG_DEVELOP, "[Journey] OnGroupSelected - Path:", group.localstatus.selected)
local _, _, e = strsplit("\001", group.localstatus.selected);
if e then
local master = group.frame.obj;
master:ReleaseChildren();
master:SetLayout("fill");
master:SetFullWidth(true);
master:SetFullHeight(true);
local f = AceGUI:Create("ScrollFrame");
f:SetLayout("flow");
master:AddChild(f);
local header = AceGUI:Create("Heading");
header:SetFullWidth(true);
f:AddChild(header);
QuestieJourneyUtils:Spacer(f);
local created = AceGUI:Create("Label");
created:SetFullWidth(true);
local entry = Questie.db.char.journey[tonumber(e)];
local day = CALENDAR_WEEKDAY_NAMES[ tonumber(date('%w', entry.Timestamp)) + 1 ];
local month = CALENDAR_FULLDATE_MONTH_NAMES[ tonumber(date('%m', entry.Timestamp)) ];
local timestamp = Questie:Colorize(date( day ..', '.. month ..' %d @ %H:%M' , entry.Timestamp), 'blue');
if entry.Event == "Note" then
header:SetText(l10n('Note: %s', entry.Title));
local note = AceGUI:Create("Label");
note:SetFullWidth(true);
note:SetText(Questie:Colorize( entry.Note , 'yellow'));
f:AddChild(note);
QuestieJourneyUtils:Spacer(f);
created:SetText(l10n('Note Created: %s', timestamp));
f:AddChild(created);
elseif entry.Event == "Level" then
header:SetText(l10n('You Reached Level %s', entry.NewLevel));
local congrats = AceGUI:Create("Label");
congrats:SetText(l10n('Congratulations! You reached %s !', entry.NewLevel));
congrats:SetFullWidth(true);
f:AddChild(congrats);
created:SetText(timestamp);
f:AddChild(created);
elseif entry.Event == "Quest" then
local state
if entry.SubType == "Accept" then
state = l10n("Accepted");
elseif entry.SubType == "Complete" then
state = l10n("Completed");
elseif entry.SubType == "Abandon" then
state = l10n("Abandoned");
else
state = "ERROR!!";
end
local quest = QuestieDB.GetQuest(entry.Quest)
if quest then
local qName = quest.name;
header:SetText(l10n('Quest %s: %s', state, qName));
local obj = AceGUI:Create("Label");
obj:SetFullWidth(true);
obj:SetText(_QuestieJourney:CreateObjectiveText(quest.Description));
f:AddChild(obj);
end
QuestieJourneyUtils:Spacer(f);
created:SetText(l10n('Quest %s: %s', state, timestamp));
f:AddChild(created);
else
header:SetText("ERROR!!");
end
end
end);
container:AddChild(journeyTreeFrame);
else
container:ReleaseChildren();
journeyTreeFrame = nil;
_QuestieJourney.myJourney:ManageTree(container);
end
end
--- Get the month and year of the latest entry in the Journey.
--- This is used to select it in the tree view.
---@return number @The month of the latest entry
---@return number @The year of the latest entry
function _QuestieJourney:GetMonthAndYearOfLatestEntry()
local journeyEntries = _QuestieJourney:GetJourneyEntries()
local years = {}
local months = {}
for year, _ in pairs(journeyEntries) do
table.insert(years, year)
end
if not next(years) then
return nil, nil
end
local maxYear = math.max(unpack(years))
for month, _ in pairs(journeyEntries[maxYear]) do
table.insert(months, month)
end
local maxMonth = math.max(unpack(months))
return maxMonth, maxYear
end
@@ -0,0 +1,102 @@
---@type QuestieJourney
local QuestieJourney = QuestieLoader:CreateModule("QuestieJourney")
local _QuestieJourney = QuestieJourney.private
-------------------------
--Import modules.
-------------------------
---@type QuestieJourneyUtils
local QuestieJourneyUtils = QuestieLoader:ImportModule("QuestieJourneyUtils");
---@type QuestieDB
local QuestieDB = QuestieLoader:ImportModule("QuestieDB");
---@type l10n
local l10n = QuestieLoader:ImportModule("l10n")
--- COMPATIBILITY ---
local CALENDAR_WEEKDAY_NAMES = QuestieCompat.CALENDAR_WEEKDAY_NAMES
local CALENDAR_FULLDATE_MONTH_NAMES = QuestieCompat.CALENDAR_FULLDATE_MONTH_NAMES
local AceGUI = LibStub("AceGUI-3.0");
--- Draw the "My Journey" tab
---@param container Frame
---@return SimpleGroup
function _QuestieJourney.myJourney:DrawTab(container)
local header = AceGUI:Create("Heading");
header:SetText(l10n('Your Recent History'));
header:SetFullWidth(true);
container:AddChild(header);
QuestieJourneyUtils:Spacer(container);
-- get last 5 elements from table for history
local counter = #Questie.db.char.journey;
local recentEvents = {};
for i = counter, counter-4, -1 do
if i <= 0 then
break;
end
recentEvents[i] = {};
recentEvents[i] = AceGUI:Create("Label");
recentEvents[i]:SetFullWidth(true);
local day = CALENDAR_WEEKDAY_NAMES[tonumber(date('%w', Questie.db.char.journey[i].Timestamp)) + 1];
local month = CALENDAR_FULLDATE_MONTH_NAMES[tonumber(date('%m', Questie.db.char.journey[i].Timestamp))];
local timestamp = Questie:Colorize(date( '[ '..day ..', '.. month ..' %d @ %H:%M ] ' , Questie.db.char.journey[i].Timestamp), 'blue');
-- if it's a quest event
if Questie.db.char.journey[i].Event == "Quest" then
local qName = QuestieDB.QueryQuestSingle(Questie.db.char.journey[i].Quest, "name");
if qName then
qName = Questie:Colorize(qName, 'gray');
if Questie.db.char.journey[i].SubType == "Accept" then
recentEvents[i]:SetText(timestamp .. Questie:Colorize(l10n('You Accepted the quest %s', qName), 'yellow'));
elseif Questie.db.char.journey[i].SubType == "Abandon" then
recentEvents[i]:SetText(timestamp .. Questie:Colorize(l10n('You Abandoned the quest %s', qName), 'yellow'));
elseif Questie.db.char.journey[i].SubType == "Complete" then
recentEvents[i]:SetText(timestamp .. Questie:Colorize(l10n('You Completed the quest %s', qName), 'yellow'));
end
end
elseif Questie.db.char.journey[i].Event == "Level" then
local level = Questie:Colorize(l10n('Level %s', Questie.db.char.journey[i].NewLevel), 'gray');
recentEvents[i]:SetText(timestamp .. Questie:Colorize(l10n('Congratulations! You reached %s !', level), 'yellow'));
elseif Questie.db.char.journey[i].Event == "Note" then
local title = Questie:Colorize(Questie.db.char.journey[i].Title, 'gray');
recentEvents[i]:SetText(timestamp .. Questie:Colorize(l10n('Note Created: %s', title), 'yellow'));
end
container:AddChild(recentEvents[i]);
end
if counter == 0 then
local justdoit = AceGUI:Create("Label");
justdoit:SetFullWidth(true);
justdoit:SetText(Questie:Colorize(l10n("It's about time you embark on your first Journey!"), 'yellow'));
container:AddChild(justdoit);
end
QuestieJourneyUtils:Spacer(container);
local treeHeader = AceGUI:Create("Heading");
treeHeader:SetText(l10n("%s's Journey", UnitName("player")));
treeHeader:SetFullWidth(true);
container:AddChild(treeHeader);
local noteButton = AceGUI:Create("Button");
noteButton:SetText(l10n('Add New Adventure Note'));
noteButton:SetPoint("RIGHT");
noteButton:SetCallback("OnClick", _QuestieJourney.ShowNotePopup);
container:AddChild(noteButton);
QuestieJourneyUtils:Spacer(container);
---@class SimpleGroup
local treeGroup = AceGUI:Create("SimpleGroup");
treeGroup:SetLayout("fill");
treeGroup:SetFullHeight(true);
treeGroup:SetFullWidth(true);
container:AddChild(treeGroup);
return treeGroup
end
+132
View File
@@ -0,0 +1,132 @@
---@type QuestieJourney
local QuestieJourney = QuestieLoader:CreateModule("QuestieJourney")
local _QuestieJourney = QuestieJourney.private
_QuestieJourney.notePopup = nil
-------------------------
--Import modules
-------------------------
---@type QuestieJourneyUtils
local QuestieJourneyUtils = QuestieLoader:ImportModule("QuestieJourneyUtils")
---@type l10n
local l10n = QuestieLoader:ImportModule("l10n")
--- COMPATIBILITY ---
local CALENDAR_WEEKDAY_NAMES = QuestieCompat.CALENDAR_WEEKDAY_NAMES
local CALENDAR_FULLDATE_MONTH_NAMES = QuestieCompat.CALENDAR_FULLDATE_MONTH_NAMES
local AceGUI = LibStub("AceGUI-3.0")
local titleBox, messageBox
local _CreateNoteWindow, _CreateContainer, _CreateDescription, _CreateTitleBox, _CreateMessageBox
local _CreateNoteAddButton, _HandleNoteEntry
function _QuestieJourney:ShowNotePopup()
if (not _QuestieJourney.notePopup) then
_QuestieJourney.notePopup = _CreateNoteWindow()
elseif (not _QuestieJourney.notePopup:IsShown()) then
_QuestieJourney.notePopup:Show()
else
_QuestieJourney.notePopup:Hide()
end
end
_CreateNoteWindow = function ()
local notePopup = AceGUI:Create("Window")
notePopup:Show()
notePopup:SetTitle(l10n('Add New Adventure Note'))
notePopup:SetWidth(400)
notePopup:SetHeight(400)
notePopup:EnableResize(false)
notePopup.frame:SetFrameStrata(_QuestieJourney.containerCache.frame:GetFrameStrata())
notePopup.frame:SetFrameLevel(_QuestieJourney.containerCache.frame:GetFrameLevel())
notePopup.frame:Raise()
notePopup:SetCallback("OnClose", function()
notePopup:Hide()
end)
local container = _CreateContainer()
notePopup:AddChild(container)
local desc = _CreateDescription()
container:AddChild(desc)
QuestieJourneyUtils:Spacer(container)
titleBox = _CreateTitleBox()
container:AddChild(titleBox)
messageBox = _CreateMessageBox()
container:AddChild(messageBox)
local addEntryBtn = _CreateNoteAddButton()
container:AddChild(addEntryBtn)
return notePopup
end
_CreateContainer = function ()
-- Setup Note Taking
local day = CALENDAR_WEEKDAY_NAMES[tonumber(date('%w', time())) + 1]
local month = CALENDAR_FULLDATE_MONTH_NAMES[tonumber(date('%m', time()))]
local today = date(day ..', '.. month ..' %d', time())
local container = AceGUI:Create("InlineGroup")
container:SetFullHeight(true)
container:SetFullWidth(true)
container:SetLayout('flow')
container:SetTitle(l10n('New Note For: %s', today))
return container
end
_CreateDescription =function ()
local desc = AceGUI:Create("Label")
desc:SetText(Questie:Colorize(l10n('Create an entry in your journal to remember a specific moment. Simply supply a title and description and Questie will remember it for you!'), 'yellow'))
desc:SetFullWidth(true)
return desc
end
_CreateTitleBox = function ()
local box = AceGUI:Create("EditBox")
box:SetFullWidth(true)
box:SetLabel(l10n('Entry Title'))
box:DisableButton(true)
box:SetFocus()
return box
end
_CreateMessageBox = function ()
local box = AceGUI:Create("MultiLineEditBox")
box:SetFullWidth(true)
box:SetNumLines(12)
box:SetLabel(l10n('Journal Entry'))
box:DisableButton(true)
return box
end
_CreateNoteAddButton = function ()
local addEntryBtn = AceGUI:Create("Button")
addEntryBtn:SetText(l10n('Add Entry'))
addEntryBtn:SetCallback("OnClick", _HandleNoteEntry)
return addEntryBtn
end
_HandleNoteEntry = function ()
local error = Questie:Colorize('[Questie] ', 'blue')
if titleBox:GetText() == '' then
print (error .. l10n('No Title was entered. You must enter a title before submitting your note.'))
return
elseif messageBox:GetText() == '' then
print (error .. l10n('No Note was entered. You must enter a note before submitting.'))
return
end
local data = {}
data.Event = "Note"
data.Note = messageBox:GetText()
data.Title = titleBox:GetText()
data.Timestamp = time()
tinsert(Questie.db.char.journey, data)
_QuestieJourney.myJourney:ManageTree(_QuestieJourney.treeCache)
_QuestieJourney.notePopup:Hide()
end
@@ -0,0 +1,236 @@
---@type QuestieJourney
local QuestieJourney = QuestieLoader:CreateModule("QuestieJourney")
local _QuestieJourney = QuestieJourney.private
_QuestieJourney.questsByZone = {}
---@type QuestieDB
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
---@type QuestieLib
local QuestieLib = QuestieLoader:ImportModule("QuestieLib")
---@type QuestieReputation
local QuestieReputation = QuestieLoader:ImportModule("QuestieReputation")
---@type QuestieCorrections
local QuestieCorrections = QuestieLoader:ImportModule("QuestieCorrections")
---@type QuestieEvent
local QuestieEvent = QuestieLoader:ImportModule("QuestieEvent")
---@type QuestieLink
local QuestieLink = QuestieLoader:ImportModule("QuestieLink")
---@type l10n
local l10n = QuestieLoader:ImportModule("l10n")
local AceGUI = LibStub("AceGUI-3.0")
local zoneTreeFrame
---Manage the zone tree itself and the contents of the per-quest window
---@param container AceSimpleGroup @The container for the zone tree
---@param zoneTree table @The zone tree table
function _QuestieJourney.questsByZone:ManageTree(container, zoneTree)
if zoneTreeFrame then
container:ReleaseChildren()
zoneTreeFrame = nil
_QuestieJourney.questsByZone:ManageTree(container, zoneTree)
return
end
zoneTreeFrame = AceGUI:Create("TreeGroup")
zoneTreeFrame:SetFullWidth(true)
zoneTreeFrame:SetFullHeight(true)
zoneTreeFrame:SetTree(zoneTree)
zoneTreeFrame.treeframe:SetWidth(220)
zoneTreeFrame:SetCallback("OnClick", function(group, ...)
local treePath = {...}
if not treePath[2] then
Questie:Debug(Questie.DEBUG_CRITICAL, "[zoneTreeFrame:OnClick] No tree path given in Journey.")
return
end
-- if they clicked on a header, don't do anything
local sel, questId = strsplit("\001", treePath[2]) -- treePath[2] looks like "a?1234" for an available quest with ID 1234
if (sel == nil or sel == "a" or sel == "p" or sel == "c" or sel == "r" or sel == "u") and (not questId) then
return
end
-- get master frame and create scroll frame inside
local master = group.frame.obj
master:ReleaseChildren()
master:SetLayout("fill")
master:SetFullWidth(true)
master:SetFullHeight(true)
---@class ScrollFrame
local scrollFrame = AceGUI:Create("ScrollFrame")
scrollFrame:SetLayout("flow")
scrollFrame:SetFullHeight(true)
master:AddChild(scrollFrame)
---@type number
questId = tonumber(questId)
local quest = QuestieDB.GetQuest(questId)
-- Add the quest to the open chat window if it was a shift click
if (IsModifiedClick("CHATLINK") and ChatEdit_GetActiveWindow()) then
ChatEdit_InsertLink(QuestieLink:GetQuestLinkString(quest.level, quest.name, quest.Id))
end
_QuestieJourney:DrawQuestDetailsFrame(scrollFrame, quest)
end)
container:AddChild(zoneTreeFrame)
end
---Get all the available/completed/repeatable/unavailable quests
---@param zoneId number @The zone ID (Check `l10n.zoneLookup`)
---@return table<number,any> @The zoneTree table which represents the list of all the different quests
function _QuestieJourney.questsByZone:CollectZoneQuests(zoneId)
local quests = QuestieJourney.zoneMap[zoneId]--QuestieDB:GetQuestsByZoneId(zoneId)
if (not quests) then
return nil
end
local zoneTree = {
[1] = {
value = "a",
text = l10n('Available Quests'),
children = {}
},
[2] = {
value = "p",
text = l10n('Missing Pre Quest'),
children = {}
},
[3] = {
value = "c",
text = l10n('Completed Quests'),
children = {}
},
[4] = {
value = "r",
text = l10n('Repeatable Quests'),
children = {},
},
[5] = {
value = "u",
text = l10n('Unobtainable Quests'),
children = {},
}
}
local sortedQuestByLevel = QuestieLib:SortQuestIDsByLevel(quests)
local availableCounter = 0
local prequestMissingCounter = 0
local completedCounter = 0
local unobtainableCounter = 0
local repeatableCounter = 0
local unobtainableQuestIds = {}
local temp = {}
for _, levelAndQuest in pairs(sortedQuestByLevel) do
---@type number
local questId = levelAndQuest[2]
-- Only show quests which are not hidden
local isStreamQuest = QuestieDB.QuestPointers and QuestieDB.QuestPointers[questId]
local isAscensionQuest = QuestieDB.ascensionQuestIds and QuestieDB.ascensionQuestIds[questId]
if QuestieCorrections.hiddenQuests
and ((not QuestieCorrections.hiddenQuests[questId]) or QuestieEvent:IsEventQuest(questId))
and (isStreamQuest or isAscensionQuest) then
temp.value = questId
temp.text = QuestieLib:GetColoredQuestName(questId, Questie.db.profile.enableTooltipsQuestLevel, false, true)
-- Completed quests
if Questie.db.char.complete[questId] then
tinsert(zoneTree[3].children, temp)
completedCounter = completedCounter + 1
else
local queryResult = QuestieDB.QueryQuest(
questId,
{
"exclusiveTo",
"nextQuestInChain",
"parentQuest",
"preQuestSingle",
"preQuestGroup",
"requiredMinRep",
"requiredMaxRep"
}
) or {}
local exclusiveTo = queryResult[1]
local nextQuestInChain = queryResult[2]
local parentQuest = queryResult[3]
local preQuestSingle = queryResult[4]
local preQuestGroup = queryResult[5]
local requiredMinRep = queryResult[6]
local requiredMaxRep = queryResult[7]
-- Exclusive quests will never be available since another quests permanently blocks them.
-- Marking them as complete should be the most satisfying solution for user
if (nextQuestInChain and Questie.db.char.complete[nextQuestInChain]) or (exclusiveTo and QuestieDB:IsExclusiveQuestInQuestLogOrComplete(exclusiveTo)) then
tinsert(zoneTree[3].children, temp)
completedCounter = completedCounter + 1
-- The parent quest has been completed
elseif parentQuest and Questie.db.char.complete[parentQuest] then
tinsert(zoneTree[3].children, temp)
completedCounter = completedCounter + 1
-- Unoptainable reputation quests
elseif not QuestieReputation:HasReputation(requiredMinRep, requiredMaxRep) then
tinsert(zoneTree[5].children, temp)
unobtainableQuestIds[questId] = true
unobtainableCounter = unobtainableCounter + 1
-- A single pre Quest is missing
elseif not QuestieDB:IsPreQuestSingleFulfilled(preQuestSingle) then
-- The pre Quest is unobtainable therefore this quest is it as well
if unobtainableQuestIds[preQuestSingle] ~= nil then
tinsert(zoneTree[5].children, temp)
unobtainableQuestIds[questId] = true
unobtainableCounter = unobtainableCounter + 1
else
tinsert(zoneTree[2].children, temp)
prequestMissingCounter = prequestMissingCounter + 1
end
-- Multiple pre Quests are missing
elseif not QuestieDB:IsPreQuestGroupFulfilled(preQuestGroup) then
local hasUnobtainablePreQuest = false
for _, preQuestId in pairs(preQuestGroup) do
if unobtainableQuestIds[preQuestId] ~= nil then
tinsert(zoneTree[5].children, temp)
unobtainableQuestIds[questId] = true
unobtainableCounter = unobtainableCounter + 1
hasUnobtainablePreQuest = true
break
end
end
if not hasUnobtainablePreQuest then
tinsert(zoneTree[2].children, temp)
prequestMissingCounter = prequestMissingCounter + 1
end
-- Repeatable quests
elseif QuestieDB.IsRepeatable(questId) then
tinsert(zoneTree[4].children, temp)
repeatableCounter = repeatableCounter + 1
-- Available quests
else
tinsert(zoneTree[1].children, temp)
availableCounter = availableCounter + 1
end
end
temp = {}
end
end
local totalCounter = availableCounter + completedCounter + prequestMissingCounter
zoneTree[1].text = zoneTree[1].text .. ' [ '.. availableCounter ..'/'.. totalCounter ..' ]'
zoneTree[2].text = zoneTree[2].text .. ' [ '.. prequestMissingCounter ..'/'.. totalCounter ..' ]'
zoneTree[3].text = zoneTree[3].text .. ' [ '.. completedCounter ..'/'.. totalCounter ..' ]'
zoneTree[4].text = zoneTree[4].text .. ' [ '.. repeatableCounter ..' ]'
zoneTree[5].text = zoneTree[5].text .. ' [ '.. unobtainableCounter ..' ]'
zoneTree.numquests = totalCounter + repeatableCounter + unobtainableCounter
return zoneTree
end
@@ -0,0 +1,165 @@
---@type QuestieJourney
local QuestieJourney = QuestieLoader:CreateModule("QuestieJourney")
local _QuestieJourney = QuestieJourney.private
-------------------------
--Import modules.
-------------------------
---@type QuestieJourneyUtils
local QuestieJourneyUtils = QuestieLoader:ImportModule("QuestieJourneyUtils")
---@type QuestiePlayer
local QuestiePlayer = QuestieLoader:ImportModule("QuestiePlayer")
---@type QuestieProfessions
local QuestieProfessions = QuestieLoader:ImportModule("QuestieProfessions")
---@type QuestieDB
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
---@type l10n
local l10n = QuestieLoader:ImportModule("l10n")
local AceGUI = LibStub("AceGUI-3.0")
local RESET = -1000
local _CreateContinentDropdown, _CreateZoneDropdown
local _HandleContinentSelection, _HandleZoneSelection
local selectedContinentId
local contDropdown, zoneDropdown, treegroup
-- function that draws the Tab for Zone Quests
function _QuestieJourney.questsByZone:DrawTab(container)
---@class AceSimpleGroup
treegroup = AceGUI:Create("SimpleGroup")
-- Header
local header = AceGUI:Create("Heading")
header:SetText(l10n('Select Your Continent and Zone'))
header:SetFullWidth(true)
container:AddChild(header)
QuestieJourneyUtils:Spacer(container)
contDropdown = _CreateContinentDropdown()
container:AddChild(contDropdown)
zoneDropdown = _CreateZoneDropdown()
container:AddChild(zoneDropdown)
QuestieJourneyUtils:Spacer(container)
header = AceGUI:Create("Heading")
header:SetText(l10n('Zone Quests'))
header:SetFullWidth(true)
container:AddChild(header)
QuestieJourneyUtils:Spacer(container)
treegroup:SetFullHeight(true)
treegroup:SetFullWidth(true)
treegroup:SetLayout("fill")
container:AddChild(treegroup)
end
_CreateContinentDropdown = function()
local dropdown = AceGUI:Create("Dropdown")
dropdown:SetList(QuestieJourney.continents)
dropdown:SetText(l10n('Select Your Continent'))
dropdown:SetCallback("OnValueChanged", _HandleContinentSelection)
local currentContinentId = QuestiePlayer:GetCurrentContinentId()
-- This mapping translates the actual continent ID to the keys of l10n.continentLookup
if currentContinentId == 0 then -- Eastern Kingdom
selectedContinentId = 1
elseif currentContinentId == 1 then -- Kalimdor
selectedContinentId = 2
elseif currentContinentId == 530 then -- Outland
selectedContinentId = 3
elseif currentContinentId == 571 then -- Northrend
selectedContinentId = 4
elseif l10n.zoneLookup[currentContinentId] then -- Dungeon
selectedContinentId = 5
end
if _QuestieJourney.lastZoneSelection[1] then
selectedContinentId = _QuestieJourney.lastZoneSelection[1]
end
dropdown:SetValue(selectedContinentId)
return dropdown
end
_CreateZoneDropdown = function()
local dropdown = AceGUI:Create("Dropdown")
local currentZoneId = QuestiePlayer:GetCurrentZoneId()
if _QuestieJourney.lastZoneSelection[2] then
currentZoneId = _QuestieJourney.lastZoneSelection[2]
end
local zones = QuestieJourney.zones[selectedContinentId]
if currentZoneId and currentZoneId > 0 and zones then
local sortedZones = QuestieJourneyUtils:GetSortedZoneKeys(zones)
dropdown:SetList(zones, sortedZones)
dropdown:SetValue(currentZoneId)
local zoneTree = _QuestieJourney.questsByZone:CollectZoneQuests(currentZoneId)
_QuestieJourney.questsByZone:ManageTree(treegroup, zoneTree)
elseif currentZoneId == RESET and zones then
dropdown:SetText(l10n('Select Your Zone'))
local sortedZones = QuestieJourneyUtils:GetSortedZoneKeys(zones)
dropdown:SetList(zones, sortedZones)
else
dropdown:SetDisabled(true)
end
dropdown:SetCallback("OnValueChanged", _HandleZoneSelection)
return dropdown
end
_HandleContinentSelection = function(key, _)
if (key.value == QuestieJourney.questCategoryKeys.CLASS) then
local _, class, _ = UnitClass("player")
local classKey = QuestieDB:GetZoneOrSortForClass(class)
local zoneTree = _QuestieJourney.questsByZone:CollectZoneQuests(classKey)
_QuestieJourney.questsByZone:ManageTree(treegroup, zoneTree)
zoneDropdown.frame:Hide()
elseif (key.value == QuestieJourney.questCategoryKeys.PROFESSIONS) then
local professionList = QuestieJourney.zones[key.value]
local playerProfessions = QuestieProfessions:GetPlayerProfessionNames()
local relevantProfessions = {}
for id, possibleName in pairs(professionList) do
for _, name in pairs(playerProfessions) do
if possibleName == name then
relevantProfessions[id] = professionList[id]
break
end
end
end
local text = l10n('Select Your Profession')
if (not next(relevantProfessions)) then
text = l10n('No Quests found')
zoneDropdown:SetDisabled(true)
else
zoneDropdown:SetDisabled(false)
end
zoneDropdown:SetList(relevantProfessions)
zoneDropdown:SetText(text)
zoneDropdown.frame:Show()
else
local sortedZones = QuestieJourneyUtils:GetSortedZoneKeys(QuestieJourney.zones[key.value])
zoneDropdown:SetList(QuestieJourney.zones[key.value], sortedZones)
zoneDropdown:SetText(l10n("Select Your Zone"))
zoneDropdown:SetDisabled(false)
zoneDropdown.frame:Show()
end
_QuestieJourney.lastZoneSelection[2] = RESET
_QuestieJourney.lastZoneSelection[1] = key.value
end
_HandleZoneSelection = function(key, _)
local zoneTree = _QuestieJourney.questsByZone:CollectZoneQuests(key.value)
_QuestieJourney.questsByZone:ManageTree(treegroup, zoneTree)
_QuestieJourney.lastZoneSelection[2] = key.value
end