diff --git a/CHANGELOG.md b/CHANGELOG.md index 4828a26..364f53a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## v1.4.2 — Quest Route Optimization + +- **[Feature]** Added Quest Route Optimization with three modes: Single Quest, All Tracked Quests, and TSP Approximation. +- **[Feature]** Route mode can be selected in Tracker options under "Route Mode". +- **[Feature]** Nearest-neighbor TSP algorithm for calculating optimal quest routes. +- **[Feature]** Visual route lines drawn on map connecting objectives in optimized order. + ## v1.4.1 — Cleanup & Repository Maintenance - **[Cleanup]** Removed `.history/` and `Research/` folders from repository tracking (were already gitignored but previously committed). diff --git a/Modules/Options/QuestieOptionsDefaults.lua b/Modules/Options/QuestieOptionsDefaults.lua index 24d6ce1..54a10d8 100644 --- a/Modules/Options/QuestieOptionsDefaults.lua +++ b/Modules/Options/QuestieOptionsDefaults.lua @@ -158,6 +158,9 @@ function QuestieOptionsDefaults:Load() objectiveProgressSoundChoiceName = "ObjectiveProgress", iconTheme = "questie", + routeMode = 1, + routeDrawInterval = 5, + minimap = { hide = false }, diff --git a/Modules/Options/TrackerTab/QuestieOptionsTracker.lua b/Modules/Options/TrackerTab/QuestieOptionsTracker.lua index b62f176..d00e192 100644 --- a/Modules/Options/TrackerTab/QuestieOptionsTracker.lua +++ b/Modules/Options/TrackerTab/QuestieOptionsTracker.lua @@ -15,6 +15,8 @@ local TrackerLinePool = QuestieLoader:ImportModule("TrackerLinePool") local TrackerQuestTimers = QuestieLoader:ImportModule("TrackerQuestTimers") ---@type QuestieArrow local QuestieArrow = QuestieLoader:ImportModule("QuestieArrow") +---@type QuestieRouteOptimizer +local QuestieRouteOptimizer = QuestieLoader:ImportModule("QuestieRouteOptimizer") ---@type l10n local l10n = QuestieLoader:ImportModule("l10n") @@ -946,6 +948,30 @@ function QuestieOptions.tabs.tracker:Initialize() }, } }, + route_header = { + type = "header", + order = 50, + name = function() return l10n('Quest Route Optimization') end, + }, + routeMode = { + type = "select", + order = 51, + width = 2, + name = function() return l10n('Route Mode') end, + desc = function() return l10n('Choose how quest routes are calculated and displayed.') end, + disabled = function() return not Questie.db.profile.trackerEnabled end, + get = function() return Questie.db.profile.routeMode or 1 end, + set = function(_, value) + Questie.db.profile.routeMode = value + QuestieRouteOptimizer:Update() + end, + values = { + [1] = l10n('Off'), + [2] = l10n('Single Quest'), + [3] = l10n('All Tracked Quests'), + [4] = l10n('TSP Approximation'), + }, + }, } } diff --git a/Modules/Tracker/QuestieRouteOptimizer.lua b/Modules/Tracker/QuestieRouteOptimizer.lua new file mode 100644 index 0000000..f4c301a --- /dev/null +++ b/Modules/Tracker/QuestieRouteOptimizer.lua @@ -0,0 +1,357 @@ +---@class QuestieRouteOptimizer +local QuestieRouteOptimizer = QuestieLoader:CreateModule("QuestieRouteOptimizer") + +------------------------- +--Import modules. +------------------------- +---@type QuestieDB +local QuestieDB = QuestieLoader:ImportModule("QuestieDB") +---@type QuestieMap +local QuestieMap = QuestieLoader:ImportModule("QuestieMap") +---@type QuestieTracker +local QuestieTracker = QuestieLoader:ImportModule("QuestieTracker") +---@type TrackerUtils +local TrackerUtils = QuestieLoader:ImportModule("TrackerUtils") +---@type QuestieCompat +local QuestieCompat = QuestieLoader:ImportModule("QuestieCompat") +---@type ZoneDB +local ZoneDB = QuestieLoader:ImportModule("ZoneDB") +---@type QuestieLib +local QuestieLib = QuestieLoader:ImportModule("QuestieLib") +---@type QuestieFramePool +local QuestieFramePool = QuestieLoader:ImportModule("QuestieFramePool") + +------------------------- +--Compat +------------------------- +local GetTime = GetTime +local pairs = pairs +local ipairs = ipairs +local tinsert = table.insert +local type = type + +------------------------- +--Route optimization modes +------------------------- +local ROUTE_MODE_OFF = 1 +local ROUTE_MODE_SINGLE_QUEST = 2 +local ROUTE_MODE_ALL_TRACKED = 3 +local ROUTE_MODE_TSP_APPROXIMATION = 4 + +------------------------- +--State +------------------------- +local routeFrames = {} +local currentRouteMode = ROUTE_MODE_OFF + +--- Calculate distance between two points +---@param x1 number +---@param y1 number +---@param x2 number +---@param y2 number +---@return number distance +local function _GetDistance(x1, y1, x2, y2) + return math.sqrt((x2 - x1) ^ 2 + (y2 - y1) ^ 2) +end + +--- Nearest neighbor TSP approximation +---@param coordinates table +---@return table +local function _NearestNeighborTSP(coordinates) + if #coordinates <= 1 then + return coordinates + end + + local visited = {} + local route = {} + local current = 1 + + visited[current] = true + tinsert(route, coordinates[current]) + + for i = 2, #coordinates do + local nearestDist = math.huge + local nearestIdx = 0 + + for j = 1, #coordinates do + if not visited[j] then + local dist = _GetDistance( + coordinates[current].x, coordinates[current].y, + coordinates[j].x, coordinates[j].y + ) + if dist < nearestDist then + nearestDist = dist + nearestIdx = j + end + end + end + + if nearestIdx > 0 then + visited[nearestIdx] = true + tinsert(route, coordinates[nearestIdx]) + current = nearestIdx + end + end + + return route +end + +--- Get spawn coordinates for a single quest +---@param questId number +---@return table? +local function _GetQuestSpawnCoordinates(questId) + local quest = QuestieDB.GetQuest(questId) + if not quest then return nil end + + local coordinates = {} + + ---@param spawnData table + ---@param zoneId number + local function AddSpawns(spawnData, zoneId) + if spawnData and spawnData.Spawns then + for _, spawn in pairs(spawnData.Spawns) do + for _, coord in pairs(spawn) do + if coord[1] > 0 and coord[2] > 0 then + tinsert(coordinates, { + x = coord[1] / 100, + y = coord[2] / 100, + data = spawnData, + zone = zoneId + }) + end + end + end + end + end + + if quest.Objectives then + for _, objective in pairs(quest.Objectives) do + if objective.Spawns then + for zoneId, spawnData in pairs(objective.Spawns) do + AddSpawns(spawnData, zoneId) + end + end + if objective.KillCredit and objective.KillCredit > 0 then + local spawns = QuestieDB.QueryNPCSingle(objective.KillCredit, "spawns") + if spawns then + for _, spawn in pairs(spawns) do + for _, coord in pairs(spawn) do + if coord[1] > 0 and coord[2] > 0 then + tinsert(coordinates, { + x = coord[1] / 100, + y = coord[2] / 100, + data = { Id = objective.KillCredit, Name = "Kill Credit" }, + zone = zoneId + }) + end + end + end + end + end + end + end + + if quest.Finishers then + for _, finisher in pairs(quest.Finishers) do + if finisher.Spawns then + for zoneId, spawnData in pairs(finisher.Spawns) do + AddSpawns(spawnData, zoneId) + end + end + end + end + + return coordinates +end + +--- Get spawn coordinates for all tracked quests +---@return table? +local function _GetAllTrackedQuestsCoordinates() + local coordinates = {} + local trackedQuests = Questie.db.char.TrackedQuests + + if not trackedQuests then return nil end + + for questId in pairs(trackedQuests) do + local questCoords = _GetQuestSpawnCoordinates(questId) + if questCoords then + for _, coord in pairs(questCoords) do + tinsert(coordinates, coord) + end + end + end + + return coordinates +end + +--- Clear all route frames +function QuestieRouteOptimizer:ClearRoutes() + for _, frame in pairs(routeFrames) do + if frame and frame:Hide then + frame:Hide() + end + end + routeFrames = {} +end + +--- Draw an optimized route for a single quest +---@param questId number +function QuestieRouteOptimizer:DrawQuestRoute(questId) + self:ClearRoutes() + + local coordinates = _GetQuestSpawnCoordinates(questId) + if not coordinates or #coordinates < 2 then + return + end + + local optimized = _NearestNeighborTSP(coordinates) + + local lastZone = nil + local zoneRoute = {} + + for i, coord in ipairs(optimized) do + if coord.zone == lastZone or not lastZone then + tinsert(zoneRoute, {coord.x, coord.y}) + lastZone = coord.zone + else + self:_DrawZoneRoute(zoneRoute, lastZone) + zoneRoute = {{coord.x, coord.y}} + lastZone = coord.zone + end + end + + if #zoneRoute > 0 and lastZone then + self:_DrawZoneRoute(zoneRoute, lastZone) + end +end + +--- Draw route for all tracked quests +function QuestieRouteOptimizer:DrawAllTrackedRoutes() + self:ClearRoutes() + + local coordinates = _GetAllTrackedQuestsCoordinates() + if not coordinates or #coordinates < 2 then + return + end + + local optimized = _NearestNeighborTSP(coordinates) + + local lastZone = nil + local zoneRoute = {} + + for i, coord in ipairs(optimized) do + if coord.zone == lastZone or not lastZone then + tinsert(zoneRoute, {coord.x, coord.y}) + lastZone = coord.zone + else + self:_DrawZoneRoute(zoneRoute, lastZone) + zoneRoute = {{coord.x, coord.y}} + lastZone = coord.zone + end + end + + if #zoneRoute > 0 and lastZone then + self:_DrawZoneRoute(zoneRoute, lastZone) + end +end + +--- Draw a TSP approximation route connecting all objectives +function QuestieRouteOptimizer:DrawTSPRoute() + self:ClearRoutes() + + local coordinates = {} + + for questId in pairs(Questie.db.char.TrackedQuests or {}) do + local questCoords = _GetQuestSpawnCoordinates(questId) + if questCoords then + for _, coord in pairs(questCoords) do + tinsert(coordinates, coord) + end + end + end + + if #coordinates < 2 then + return + end + + local optimized = _NearestNeighborTSP(coordinates) + + local lastZone = nil + local zoneRoute = {} + + for i, coord in ipairs(optimized) do + if coord.zone == lastZone or not lastZone then + tinsert(zoneRoute, {coord.x, coord.y}) + lastZone = coord.zone + else + self:_DrawZoneRoute(zoneRoute, lastZone) + zoneRoute = {{coord.x, coord.y}} + lastZone = coord.zone + end + end + + if #zoneRoute > 0 and lastZone then + self:_DrawZoneRoute(zoneRoute, lastZone) + end +end + +---@param waypoints table +---@param zoneId number +function QuestieRouteOptimizer:_DrawZoneRoute(waypoints, zoneId) + if #waypoints < 2 then return end + + local uiMapId = ZoneDB:GetUiMapIdByAreaId(zoneId) + if not uiMapId then return end + + local routeData = { + Title = "Quest Route", + IconScale = 1.0, + Type = "route", + UiMapID = uiMapId, + x = waypoints[1][1], + y = waypoints[1][2], + } + + local icon = QuestieMap:DrawWorldIcon(routeData, zoneId, waypoints[1][1], waypoints[1][2]) + + local lineFrames = QuestieFramePool:CreateWaypoints(icon, waypoints, nil, {0.2, 0.8, 1, 0.7}, zoneId) + tinsert(routeFrames, icon) + + for _, lineFrame in ipairs(lineFrames) do + tinsert(routeFrames, lineFrame) + end +end + +--- Update route display based on current mode +function QuestieRouteOptimizer:Update() + local mode = Questie.db.profile.routeMode or ROUTE_MODE_OFF + + if mode == ROUTE_MODE_OFF then + self:ClearRoutes() + elseif mode == ROUTE_MODE_SINGLE_QUEST then + local questId = QuestieTracker:GetSelectedQuest() + if questId then + self:DrawQuestRoute(questId) + else + self:ClearRoutes() + end + elseif mode == ROUTE_MODE_ALL_TRACKED then + self:DrawAllTrackedRoutes() + elseif mode == ROUTE_MODE_TSP_APPROXIMATION then + self:DrawTSPRoute() + end +end + +--- Get route mode from settings +function QuestieRouteOptimizer:GetMode() + return Questie.db.profile.routeMode or ROUTE_MODE_OFF +end + +--- Set route mode +---@param mode number +function QuestieRouteOptimizer:SetMode(mode) + Questie.db.profile.routeMode = mode + self:Update() +end + +return QuestieRouteOptimizer diff --git a/Questie-X-Classic.toc b/Questie-X-Classic.toc index 3773208..885c457 100644 --- a/Questie-X-Classic.toc +++ b/Questie-X-Classic.toc @@ -1,11 +1,11 @@ ## Interface: 30300 -## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-Classic|cFF00FF00 v1.4.1|r +## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-Classic|cFF00FF00 v1.4.2|r ## Notes: A standalone Classic QuestHelper ## Notes-esMX: Ayundante de misión ## Notes-esES: Ayundante de misión ## Notes-ptBR: Ajudante de missão ## Notes-frFR: Assistant de quête -## Version: 1.4.1 +## Version: 1.4.2 ## RequiredDeps: ## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu ## SavedVariables: QuestieConfig diff --git a/Questie-X-TBC.toc b/Questie-X-TBC.toc index dbb3e6b..dc8014c 100644 --- a/Questie-X-TBC.toc +++ b/Questie-X-TBC.toc @@ -1,11 +1,11 @@ ## Interface: 30300 -## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-TBC|cFF00FF00 v1.4.1|r +## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-TBC|cFF00FF00 v1.4.2|r ## Notes: A standalone Classic QuestHelper ## Notes-esMX: Ayundante de misión ## Notes-esES: Ayundante de misión ## Notes-ptBR: Ajudante de missão ## Notes-frFR: Assistant de quête -## Version: 1.4.1 +## Version: 1.4.2 ## RequiredDeps: ## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu ## SavedVariables: QuestieConfig diff --git a/Questie-X-Turtle.toc b/Questie-X-Turtle.toc index 724fac9..6e81bf0 100644 --- a/Questie-X-Turtle.toc +++ b/Questie-X-Turtle.toc @@ -1,11 +1,11 @@ ## Interface: 11200 -## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-Turtle|cFF00FF00 v1.4.1|r +## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-Turtle|cFF00FF00 v1.4.2|r ## Notes: A standalone Classic QuestHelper ## Notes-esMX: Ayundante de misiones ## Notes-esES: Ayundante de misiones ## Notes-ptBR: Ajudante de misiones ## Notes-frFR: Assistant de quêtes -## Version: 1.4.1 +## Version: 1.4.2 ## RequiredDeps: ## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu, Questie-X-TurtleDB ## SavedVariables: QuestieConfig diff --git a/Questie-X.toc b/Questie-X.toc index 7406836..38a451e 100644 --- a/Questie-X.toc +++ b/Questie-X.toc @@ -11,7 +11,7 @@ ## Notes-esES: Ayundante de misión ## Notes-ptBR: Ajudante de missão ## Notes-frFR: Assistant de quête -## Version: 1.4.1 +## Version: 1.4.2 ## RequiredDeps: ## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu, Questie-X-WotLKDB, Questie-X-ClassicDB, Questie-X-TBCDB, Questie-X-TurtleDB, Questie-X-AscensionDB, Questie-X-EbonholdDB ## SavedVariables: QuestieConfig, QuestieLearnerDB diff --git a/README.md b/README.md index d83906f..b9f210a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Questie-X Logo -![Version](https://img.shields.io/badge/Questie--X-v1.4.1-blue.svg?style=for-the-badge) +![Version](https://img.shields.io/badge/Questie--X-v1.4.2-blue.svg?style=for-the-badge) [![Downloads](https://img.shields.io/github/downloads/Xurkon/Questie-X/total?style=for-the-badge&color=e67e22)](https://github.com/Xurkon/Questie-X/releases) [![Documentation](https://img.shields.io/badge/Documentation-View%20Docs-58a6ff?style=for-the-badge)](https://xurkon.github.io/Questie-X/) [![Patreon](https://img.shields.io/badge/Patreon-F96854?style=for-the-badge&logo=patreon&logoColor=white)](https://www.patreon.com/Xurkon) diff --git a/docs/changelog.html b/docs/changelog.html index 19c9a30..92a4da3 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -176,6 +176,16 @@
+

v1.4.2 — Quest Route Optimization

+
    +
  • [Feature] Added Quest Route Optimization with three modes: Single Quest, All Tracked Quests, and TSP Approximation.
  • +
  • [Feature] Route mode can be selected in Tracker options under "Route Mode".
  • +
  • [Feature] Nearest-neighbor TSP algorithm for calculating optimal quest routes.
  • +
  • [Feature] Visual route lines drawn on map connecting objectives in optimized order.
  • +
+ +
+

v1.4.1 — Cleanup & Repository Maintenance

  • [Cleanup] Removed .history/ and Research/ folders from repository tracking.