diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ff4876..4802966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - **[Fix — Killcredit NPC Linking]** Enhanced `killcredit` objective handling to properly link NPCs when database entries have missing or invalid IDs. Added two-pass approach: first tries ID-based lookup, then falls back to name-based search using the objective description text. Also fixed `monster` function to gracefully handle `npcId <= 0` instead of erroring. - **[Feature — Existing Quest Scanning]** Added `ScanExistingQuestLog` function to proactively map objectives for quests already in the quest log on addon initialization. Previously, objective mapping only occurred for newly accepted quests. - **[Fix — AceComm-3.0]** Added nil check for `RegisterAddonMessagePrefix` on private servers where this API may not be available. +- **[Feature — Use Quest Item Keybind]** Added configurable keyboard hotkey to automatically use the quest item for the nearest incomplete quest objective. When pressed, Questie scans active quests for usable quest items (items with spells), checks which ones are in the player's bags, calculates proximity to quest objectives, and uses the nearest one. Configurable via Tracker options tab. ## v1.5.0 (2026-03-28) diff --git a/Modules/Options/QuestieOptionsDefaults.lua b/Modules/Options/QuestieOptionsDefaults.lua index 24d6ce1..ef6d2a1 100644 --- a/Modules/Options/QuestieOptionsDefaults.lua +++ b/Modules/Options/QuestieOptionsDefaults.lua @@ -100,6 +100,7 @@ function QuestieOptionsDefaults:Load() trackerbindSetTomTom = 'ctrlleft', trackerbindOpenQuestLog = 'left', trackerbindUntrack = "shiftleft", + useQuestItemKeybind = '', trackerSetpoint = "TOPLEFT", trackerFontSizeHeader = 12, trackerFontHeader = 'Friz Quadrata TT', diff --git a/Modules/Options/TrackerTab/QuestieOptionsTracker.lua b/Modules/Options/TrackerTab/QuestieOptionsTracker.lua index b62f176..e6746ef 100644 --- a/Modules/Options/TrackerTab/QuestieOptionsTracker.lua +++ b/Modules/Options/TrackerTab/QuestieOptionsTracker.lua @@ -573,6 +573,20 @@ function QuestieOptions.tabs.tracker:Initialize() Questie.db.profile.trackerbindSetTomTom = key end }, + useQuestItemKeybind = { + type = "keybinding", + order = 10.5, + name = function() return l10n('Use Quest Item (Nearest)') end, + desc = function() return l10n('Press this keybind to automatically use a quest item for the nearest incomplete quest objective. The item must be in your bags and be a usable quest item (trigger a spell when used).') end, + disabled = function() return not Questie.db.profile.trackerEnabled end, + get = function() return Questie.db.profile.useQuestItemKeybind end, + set = function(_, key) + Questie.db.profile.useQuestItemKeybind = key + if QuestieTracker_UpdateQuestItemKeybind then + QuestieTracker_UpdateQuestItemKeybind() + end + end + }, trackerSetpoint = { type = "select", order = 11, diff --git a/Modules/Tracker/QuestieTracker.lua b/Modules/Tracker/QuestieTracker.lua index 6c60c43..a4b2d7a 100644 --- a/Modules/Tracker/QuestieTracker.lua +++ b/Modules/Tracker/QuestieTracker.lua @@ -170,6 +170,51 @@ function QuestieTracker:PruneGhostQuests() return removedAny end +local questItemKeybindFrame = nil +questItemUseFrame = nil + +function QuestieTracker_UpdateQuestItemKeybind() + if not questItemKeybindFrame then + return + end + + local keybind = Questie and Questie.db and Questie.db.profile and Questie.db.profile.useQuestItemKeybind + + ClearOverrideBindings(questItemKeybindFrame) + + if keybind and keybind ~= "" then + local upperKeybind = string.upper(keybind) + SetOverrideBinding(questItemKeybindFrame, true, upperKeybind, "CLICK Questie_QuestItemUseBtn:LeftButton") + end +end + +local function _InstallQuestItemKeybindHandler() + if questItemKeybindFrame then + return + end + + questItemKeybindFrame = CreateFrame("Frame", "Questie_QuestItemKeybindFrame") + + questItemUseFrame = CreateFrame("Button", "Questie_QuestItemUseBtn", questItemKeybindFrame, "SecureActionButtonTemplate") + questItemUseFrame:SetAttribute("type", "item") + questItemUseFrame:Hide() + + questItemKeybindFrame:SetScript("OnEvent", function(self, event) + if event == "PLAYER_LOGIN" then + C_Timer.After(0.5, QuestieTracker_UpdateQuestItemKeybind) + elseif event == "PLAYER_REGEN_ENABLED" then + ClearOverrideBindings(questItemKeybindFrame) + end + end) + + questItemKeybindFrame:RegisterEvent("PLAYER_LOGIN") + questItemKeybindFrame:RegisterEvent("PLAYER_REGEN_ENABLED") + + if IsLoggedIn() then + C_Timer.After(0.5, QuestieTracker_UpdateQuestItemKeybind) + end +end + local function _InstallQuestLogUpdateListener() if QuestieTracker._questLogUpdateListenerInstalled then return end @@ -256,6 +301,9 @@ function QuestieTracker.Initialize() -- Note: _InstallMissingQuestLogWarningFilter was removed (fix #1). -- Ghost quest warnings are now prevented upstream in PruneGhostQuests. + -- Initialize keyboard handler for Use Quest Item keybind + _InstallQuestItemKeybindHandler() + TrackerFadeTicker.Initialize(trackerBaseFrame, trackerHeaderFrame) QuestieTracker.started = true @@ -1230,7 +1278,7 @@ function QuestieTracker:Update() -- Add incomplete Quest Objectives if complete == 0 and quest.isComplete ~= true then for _, objective in pairs(quest.Objectives) do - if objective and (not Questie.db.profile.hideCompletedQuestObjectives or (Questie.db.profile.hideCompletedQuestObjectives and objective.Needed ~= objective.Collected)) then + if objective and objective.Description and (not Questie.db.profile.hideCompletedQuestObjectives or (Questie.db.profile.hideCompletedQuestObjectives and objective.Needed ~= objective.Collected)) then -- Get next line in linePool line = TrackerLinePool.GetNextLine() diff --git a/Modules/Tracker/TrackerUtils.lua b/Modules/Tracker/TrackerUtils.lua index 0b79bc0..2a5d36f 100644 --- a/Modules/Tracker/TrackerUtils.lua +++ b/Modules/Tracker/TrackerUtils.lua @@ -656,41 +656,7 @@ local function _GetContinent(uiMapId) end end -local function _GetZoneName(zoneOrSort, questId) - if not zoneOrSort then return "Unknown Zone" end - local zoneName - local sortObj = Questie.db.profile.trackerSortObjectives - if sortObj == "byZone" or sortObj == "byZonePlayerProximity" or sortObj == "byZonePlayerProximityReversed" then - if (zoneOrSort) > 0 then - -- Valid ZoneID - zoneName = TrackerUtils:GetZoneNameByID(zoneOrSort) - elseif (zoneOrSort) < 0 then - -- Valid CategoryID - zoneName = TrackerUtils:GetCategoryNameByID(zoneOrSort) - else - -- The quest has no explicit zone or category. Fallback to "Unknown Zone" - zoneName = "Unknown Zone" - Questie:Debug(Questie.DEBUG_CRITICAL, "[TrackerUtils:_GetZoneName] zoneOrSort", zoneOrSort, "of quest", - questId, "is not in the Database!") - end - else - -- Let's create custom Zones based on Sorting type. - if sortObj == "byComplete" then - zoneName = "Quests (By %% Complete)" - elseif sortObj == "byCompleteReversed" then - zoneName = "Quests (By %% Complete Reversed)" - elseif sortObj == "byLevel" then - zoneName = "Quests (By Level)" - elseif sortObj == "byLevelReversed" then - zoneName = "Quests (By Level Reversed)" - elseif sortObj == "byProximity" then - zoneName = "Quests (By Proximity)" - elseif sortObj == "byProximityReversed" then - zoneName = "Quests (By Proximity Reversed)" - end - end - return zoneName -end + ---@return table sortedQuestIds Table with sorted Quest ID's by Sort Type ---@return table questDetails Table with raw quest table from QuestiePlayer.currentQuestLog, percentage completed value per quest, and a "translated" zoneName @@ -727,6 +693,47 @@ local function GetQuestLogZoneName(questId) return nil end +local function _GetZoneName(zoneOrSort, questId, zoneNameOverride) + if zoneNameOverride and zoneNameOverride ~= "" then + return zoneNameOverride + end + if not zoneOrSort then return "Unknown Zone" end + local zoneName + local sortObj = Questie.db.profile.trackerSortObjectives + if sortObj == "byZone" or sortObj == "byZonePlayerProximity" or sortObj == "byZonePlayerProximityReversed" then + if (zoneOrSort) > 0 then + zoneName = TrackerUtils:GetZoneNameByID(zoneOrSort) + if not zoneName or zoneName == "Unknown Zone" then + local logZone = GetQuestLogZoneName(questId) + if logZone then + zoneName = logZone + end + end + elseif (zoneOrSort) < 0 then + zoneName = TrackerUtils:GetCategoryNameByID(zoneOrSort) + else + zoneName = "Unknown Zone" + Questie:Debug(Questie.DEBUG_CRITICAL, "[TrackerUtils:_GetZoneName] zoneOrSort", zoneOrSort, "of quest", + questId, "is not in the Database!") + end + else + if sortObj == "byComplete" then + zoneName = "Quests (By %% Complete)" + elseif sortObj == "byCompleteReversed" then + zoneName = "Quests (By %% Complete Reversed)" + elseif sortObj == "byLevel" then + zoneName = "Quests (By Level)" + elseif sortObj == "byLevelReversed" then + zoneName = "Quests (By Level Reversed)" + elseif sortObj == "byProximity" then + zoneName = "Quests (By Proximity)" + elseif sortObj == "byProximityReversed" then + zoneName = "Quests (By Proximity Reversed)" + end + end + return zoneName +end + -- Returns nil if the quest is not currently in the quest log. function TrackerUtils:BuildFallbackQuest(questId) for i = 1, GetNumQuestLogEntries() do @@ -763,6 +770,10 @@ function TrackerUtils:BuildFallbackQuest(questId) end end local zoneId = (zoneText and GetAreaIdByZoneName(zoneText)) or 0 + local zoneNameOverride = nil + if zoneText and (not zoneId or zoneId == 0) then + zoneNameOverride = zoneText + end local quest = { Id = questId, @@ -770,6 +781,7 @@ function TrackerUtils:BuildFallbackQuest(questId) level = level or 0, zoneOrSort = zoneId, zoneName = zoneText, + zoneNameOverride = zoneNameOverride, Objectives = objectives, SpecialObjectives = {}, isFallback = true, @@ -829,7 +841,13 @@ function TrackerUtils:GetSortedQuestIds() local logZone = GetQuestLogZoneName(capturedId) if logZone then quest.zoneName = logZone - quest.zoneOrSort = GetAreaIdByZoneName(logZone) or 0 + local areaId = GetAreaIdByZoneName(logZone) + if areaId and areaId > 0 then + quest.zoneOrSort = areaId + else + quest.zoneOrSort = 0 + quest.zoneNameOverride = logZone + end end end QuestiePlayer.currentQuestlog[qid] = quest @@ -860,7 +878,7 @@ function TrackerUtils:GetSortedQuestIds() -- Create questDetails table keys and insert values questDetails[qid] = {} questDetails[qid].quest = quest - questDetails[qid].zoneName = quest.zoneName or _GetZoneName(quest.zoneOrSort, qid) + questDetails[qid].zoneName = _GetZoneName(quest.zoneOrSort, qid, quest.zoneNameOverride) if quest:IsComplete() == 1 or (not next(quest.Objectives)) then questDetails[qid].questCompletePercent = 1 @@ -1279,3 +1297,70 @@ function TrackerUtils:UpdateVoiceOverPlayButtons() end end end + +function TrackerUtils:UseNearestQuestItem() + if InCombatLockdown() then + Questie:Debug(Questie.DEBUG_INFO, "[TrackerUtils:UseNearestQuestItem] Cannot use items while in combat") + return + end + + local playerPosition = _GetWorldPlayerPosition() + if not playerPosition then + Questie:Debug(Questie.DEBUG_INFO, "[TrackerUtils:UseNearestQuestItem] Could not get player position") + return + end + + local bestDistance = math.huge + local bestQuestIndex = nil + local bestItemName = nil + + local numEntries, numQuests = GetNumQuestLogEntries() + + for i = 1, numEntries do + local title, level, questTag, isHeader, isCollapsed, isComplete, isDaily, questId = GetQuestLogTitle(i) + + if not isHeader and questId then + local itemInfo = GetQuestLogSpecialItemInfo(i) + + if itemInfo then + local itemName + if type(itemInfo) == "string" then + itemName = itemInfo + else + itemName = tostring(itemInfo) + end + + local quest = QuestieDB.GetQuest(questId) + if quest and quest:IsComplete() ~= 1 then + local spawn, zone = QuestieMap:GetNearestQuestSpawn(quest) + if spawn and zone then + local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone) + if uiMapId then + local _, worldPosition = C_Map.GetWorldPosFromMapPos(uiMapId, { + x = spawn[1] / 100, + y = spawn[2] / 100 + }) + + if worldPosition then + local distance = _GetDistance(playerPosition.x, playerPosition.y, worldPosition.x, worldPosition.y) + + if distance < bestDistance then + bestDistance = distance + bestQuestIndex = i + bestItemName = itemName + end + end + end + end + end + end + end + end + + if bestQuestIndex and bestItemName then + Questie:Debug(Questie.DEBUG_INFO, "[TrackerUtils:UseNearestQuestItem] Using item:", bestItemName, "for quest index:", bestQuestIndex, "distance:", bestDistance) + UseItemByName(bestItemName) + else + Questie:Debug(Questie.DEBUG_INFO, "[TrackerUtils:UseNearestQuestItem] No usable quest item found") + end +end