feat: Add Quest Route Optimization feature

This commit is contained in:
Xurkon
2026-03-19 21:30:05 -05:00
parent eba0d39ca6
commit 971d780a9b
10 changed files with 411 additions and 8 deletions
+7
View File
@@ -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).
@@ -158,6 +158,9 @@ function QuestieOptionsDefaults:Load()
objectiveProgressSoundChoiceName = "ObjectiveProgress",
iconTheme = "questie",
routeMode = 1,
routeDrawInterval = 5,
minimap = {
hide = false
},
@@ -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'),
},
},
}
}
+357
View File
@@ -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<number, {x: number, y: number, data: any}>
---@return table<number, {x: number, y: number, data: any}>
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<number, {x: number, y: number, data: any}>?
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<number, {x: number, y: number, data: any}>?
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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -2,7 +2,7 @@
<img src="docs/QuestieXlogo.png" alt="Questie-X Logo" width="320" />
![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)
+10
View File
@@ -176,6 +176,16 @@
</div>
<div class="container">
<h2 id="v142">v1.4.2 — Quest Route Optimization</h2>
<ul>
<li><strong>[Feature]</strong> Added Quest Route Optimization with three modes: Single Quest, All Tracked Quests, and TSP Approximation.</li>
<li><strong>[Feature]</strong> Route mode can be selected in Tracker options under "Route Mode".</li>
<li><strong>[Feature]</strong> Nearest-neighbor TSP algorithm for calculating optimal quest routes.</li>
<li><strong>[Feature]</strong> Visual route lines drawn on map connecting objectives in optimized order.</li>
</ul>
<hr>
<h2 id="v141">v1.4.1 — Cleanup &amp; Repository Maintenance</h2>
<ul>
<li><strong>[Cleanup]</strong> Removed <code>.history/</code> and <code>Research/</code> folders from repository tracking.</li>