fix: rewrite QuestRouteOptimizer module

This commit is contained in:
Xurkon
2026-03-19 21:48:02 -05:00
parent 7857340c37
commit 156a184da4
2 changed files with 160 additions and 189 deletions
+28
View File
@@ -21,6 +21,8 @@ local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
local l10n = QuestieLoader:ImportModule("l10n") local l10n = QuestieLoader:ImportModule("l10n")
---@type QuestieCombatQueue ---@type QuestieCombatQueue
local QuestieCombatQueue = QuestieLoader:ImportModule("QuestieCombatQueue") local QuestieCombatQueue = QuestieLoader:ImportModule("QuestieCombatQueue")
---@type QuestieRouteOptimizer
local QuestieRouteOptimizer = QuestieLoader:ImportModule("QuestieRouteOptimizer")
function QuestieSlash.RegisterSlashCommands() function QuestieSlash.RegisterSlashCommands()
@@ -64,6 +66,7 @@ function QuestieSlash.HandleCommands(input)
print(Questie:Colorize("/questie doable [questID] - " .. l10n("Prints whether you are eligibile to do a quest"), "yellow")); print(Questie:Colorize("/questie doable [questID] - " .. l10n("Prints whether you are eligibile to do a quest"), "yellow"));
print(Questie:Colorize("/questie version - " .. l10n("Prints Questie and client version info"), "yellow")); print(Questie:Colorize("/questie version - " .. l10n("Prints Questie and client version info"), "yellow"));
print(Questie:Colorize("/questie learn [toggle/stats/clear/export] - " .. l10n("Self-learning database: toggle on/off, view stats, clear data, or export"), "yellow")); print(Questie:Colorize("/questie learn [toggle/stats/clear/export] - " .. l10n("Self-learning database: toggle on/off, view stats, clear data, or export"), "yellow"));
print(Questie:Colorize("/questie route [off/single/all/tsp] - " .. l10n("Toggle quest route optimization. off=disable, single=single quest, all=all tracked, tsp=TSP approximation"), "yellow"));
return; return;
end end
@@ -218,5 +221,30 @@ function QuestieSlash.HandleCommands(input)
return return
end end
-- /questie route [off/single/all/tsp]
if mainCommand == "route" then
local ROUTE_MODE_OFF = 1
local ROUTE_MODE_SINGLE_QUEST = 2
local ROUTE_MODE_ALL_TRACKED = 3
local ROUTE_MODE_TSP_APPROXIMATION = 4
if subCommand == "off" or not subCommand then
QuestieRouteOptimizer:SetMode(ROUTE_MODE_OFF)
Questie:Print("Quest route " .. l10n("disabled"))
elseif subCommand == "single" then
QuestieRouteOptimizer:SetMode(ROUTE_MODE_SINGLE_QUEST)
Questie:Print("Quest route " .. l10n("set to single quest mode"))
elseif subCommand == "all" then
QuestieRouteOptimizer:SetMode(ROUTE_MODE_ALL_TRACKED)
Questie:Print("Quest route " .. l10n("set to all tracked quests mode"))
elseif subCommand == "tsp" then
QuestieRouteOptimizer:SetMode(ROUTE_MODE_TSP_APPROXIMATION)
Questie:Print("Quest route " .. l10n("set to TSP approximation mode"))
else
Questie:Print("Usage: /questie route [off/single/all/tsp]")
end
return
end
print(Questie:Colorize("[Questie] ", "yellow") .. l10n("Invalid command. For a list of options please type: ") .. Questie:Colorize("/questie help", "yellow")); print(Questie:Colorize("[Questie] ", "yellow") .. l10n("Invalid command. For a list of options please type: ") .. Questie:Colorize("/questie help", "yellow"));
end end
+132 -189
View File
@@ -10,25 +10,21 @@ local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
local QuestieMap = QuestieLoader:ImportModule("QuestieMap") local QuestieMap = QuestieLoader:ImportModule("QuestieMap")
---@type QuestieTracker ---@type QuestieTracker
local QuestieTracker = QuestieLoader:ImportModule("QuestieTracker") local QuestieTracker = QuestieLoader:ImportModule("QuestieTracker")
---@type TrackerUtils
local TrackerUtils = QuestieLoader:ImportModule("TrackerUtils")
---@type QuestieCompat ---@type QuestieCompat
local QuestieCompat = QuestieLoader:ImportModule("QuestieCompat") local QuestieCompat = QuestieLoader:ImportModule("QuestieCompat")
---@type ZoneDB
local ZoneDB = QuestieLoader:ImportModule("ZoneDB")
---@type QuestieLib
local QuestieLib = QuestieLoader:ImportModule("QuestieLib")
---@type QuestieFramePool ---@type QuestieFramePool
local QuestieFramePool = QuestieLoader:ImportModule("QuestieFramePool") local QuestieFramePool = QuestieLoader:ImportModule("QuestieFramePool")
---@type QuestieLib
local QuestieLib = QuestieLoader:ImportModule("QuestieLib")
---@type l10n
local l10n = QuestieLoader:ImportModule("l10n")
------------------------- -------------------------
--Compat --Compat
------------------------- -------------------------
local GetTime = GetTime
local pairs = pairs local pairs = pairs
local ipairs = ipairs local ipairs = ipairs
local tinsert = table.insert local tinsert = table.insert
local type = type
------------------------- -------------------------
--Route optimization modes --Route optimization modes
@@ -41,8 +37,14 @@ local ROUTE_MODE_TSP_APPROXIMATION = 4
------------------------- -------------------------
--State --State
------------------------- -------------------------
local routeFrames = {} local routeIcons = {}
local currentRouteMode = ROUTE_MODE_OFF local routeLines = {}
-------------------------
--Constants
-------------------------
local ROUTE_COLOR = {0.2, 0.8, 1, 0.8}
local WAYPOINT_ICON = "Interface\\WorldMap\\WorldMapPartyIcon"
--- Calculate distance between two points --- Calculate distance between two points
---@param x1 number ---@param x1 number
@@ -54,9 +56,21 @@ local function _GetDistance(x1, y1, x2, y2)
return math.sqrt((x2 - x1) ^ 2 + (y2 - y1) ^ 2) return math.sqrt((x2 - x1) ^ 2 + (y2 - y1) ^ 2)
end end
--- Get player position on current map
---@return number? x
---@return number? y
local function _GetPlayerPosition()
local mapID = GetCurrentMapAreaID()
local x, y = GetPlayerMapPosition(mapID)
if x == 0 and y == 0 then
x, y = GetPlayerMapPosition("player")
end
return x, y
end
--- Nearest neighbor TSP approximation --- Nearest neighbor TSP approximation
---@param coordinates table<number, {x: number, y: number, data: any}> ---@param coordinates table<number, {x: number, y: number, zone: number}>
---@return table<number, {x: number, y: number, data: any}> ---@return table<number, {x: number, y: number, zone: number}>
local function _NearestNeighborTSP(coordinates) local function _NearestNeighborTSP(coordinates)
if #coordinates <= 1 then if #coordinates <= 1 then
return coordinates return coordinates
@@ -96,28 +110,32 @@ local function _NearestNeighborTSP(coordinates)
return route return route
end end
--- Get spawn coordinates for a single quest --- Get spawn coordinates for objectives in a quest
---@param questId number ---@param questId number
---@return table<number, {x: number, y: number, data: any}>? ---@return table<number, {x: number, y: number, zone: number}>?
local function _GetQuestSpawnCoordinates(questId) local function _GetQuestObjectiveCoords(questId)
local quest = QuestieDB.GetQuest(questId) local quest = QuestieDB.GetQuest(questId)
if not quest then return nil end if not quest then return nil end
local coordinates = {} local coordinates = {}
local currentMapId = GetCurrentMapAreaID()
---@param spawnData table ---@param spawns table?
---@param zoneId number ---@param zoneId number
local function AddSpawns(spawnData, zoneId) local function AddCoords(spawns, zoneId)
if spawnData and spawnData.Spawns then if not spawns then return end
for _, spawn in pairs(spawnData.Spawns) do for _, spawn in pairs(spawns) do
if type(spawn) == "table" then
for _, coord in pairs(spawn) do for _, coord in pairs(spawn) do
if coord[1] > 0 and coord[2] > 0 then if type(coord) == "table" and coord[1] and coord[2] then
tinsert(coordinates, { local x, y = coord[1], coord[2]
x = coord[1] / 100, if x > 0 and x <= 100 and y > 0 and y <= 100 then
y = coord[2] / 100, tinsert(coordinates, {
data = spawnData, x = x / 100,
zone = zoneId y = y / 100,
}) zone = zoneId
})
end
end end
end end
end end
@@ -128,34 +146,7 @@ local function _GetQuestSpawnCoordinates(questId)
for _, objective in pairs(quest.Objectives) do for _, objective in pairs(quest.Objectives) do
if objective.Spawns then if objective.Spawns then
for zoneId, spawnData in pairs(objective.Spawns) do for zoneId, spawnData in pairs(objective.Spawns) do
AddSpawns(spawnData, zoneId) AddCoords(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
end end
@@ -165,15 +156,15 @@ local function _GetQuestSpawnCoordinates(questId)
end end
--- Get spawn coordinates for all tracked quests --- Get spawn coordinates for all tracked quests
---@return table<number, {x: number, y: number, data: any}>? ---@return table<number, {x: number, y: number, zone: number}>?
local function _GetAllTrackedQuestsCoordinates() local function _GetAllTrackedQuestCoords()
local coordinates = {} local coordinates = {}
local trackedQuests = Questie.db.char.TrackedQuests local trackedQuests = Questie.db.char.TrackedQuests
if not trackedQuests then return nil end if not trackedQuests then return nil end
for questId in pairs(trackedQuests) do for questId in pairs(trackedQuests) do
local questCoords = _GetQuestSpawnCoordinates(questId) local questCoords = _GetQuestObjectiveCoords(questId)
if questCoords then if questCoords then
for _, coord in pairs(questCoords) do for _, coord in pairs(questCoords) do
tinsert(coordinates, coord) tinsert(coordinates, coord)
@@ -184,145 +175,78 @@ local function _GetAllTrackedQuestsCoordinates()
return coordinates return coordinates
end end
--- Clear all route frames --- Clear all route visuals
function QuestieRouteOptimizer:ClearRoutes() function QuestieRouteOptimizer:ClearRoutes()
for _, frame in pairs(routeFrames) do for _, icon in pairs(routeIcons) do
if frame and frame:Hide then if icon and icon.Hide then
frame:Hide() icon:Hide()
end end
end end
routeFrames = {} routeIcons = {}
for _, line in pairs(routeLines) do
if line and line.Hide then
line:Hide()
end
end
routeLines = {}
end end
--- Draw an optimized route for a single quest --- Draw a route line between waypoints
---@param questId number ---@param waypoints table<number, {x: number, y: 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 ---@param zoneId number
function QuestieRouteOptimizer:_DrawZoneRoute(waypoints, zoneId) local function _DrawRouteLine(waypoints, zoneId)
if #waypoints < 2 then return end if #waypoints < 2 then return end
local uiMapId = ZoneDB:GetUiMapIdByAreaId(zoneId) local icon = CreateFrame("Button", "QuestieRouteIcon" .. #routeIcons, UIParent)
if not uiMapId then return end icon:SetWidth(1)
icon:SetHeight(1)
local routeData = { icon:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
Title = "Quest Route", icon:SetFrameLevel(1)
IconScale = 1.0, icon:Show()
Type = "route", tinsert(routeIcons, icon)
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)
local lineFrames = QuestieFramePool:CreateWaypoints(icon, waypoints, 2, ROUTE_COLOR, zoneId)
for _, lineFrame in ipairs(lineFrames) do for _, lineFrame in ipairs(lineFrames) do
tinsert(routeFrames, lineFrame) lineFrame:Show()
tinsert(routeLines, lineFrame)
end end
end end
--- Update route display based on current mode --- Draw optimized route for given coordinates
---@param coordinates table<number, {x: number, y: number, zone: number}>
function QuestieRouteOptimizer:DrawRoute(coordinates)
self:ClearRoutes()
if not coordinates or #coordinates < 2 then
return
end
local optimized = _NearestNeighborTSP(coordinates)
local currentMapId = GetCurrentMapAreaID()
local mapWaypoints = {}
local currentZone = nil
for _, coord in ipairs(optimized) do
if coord.zone == currentZone or not currentZone then
tinsert(mapWaypoints, {coord.x, coord.y})
currentZone = coord.zone
else
if #mapWaypoints >= 2 then
_DrawRouteLine(mapWaypoints, currentZone)
end
mapWaypoints = {{coord.x, coord.y}}
currentZone = coord.zone
end
end
if #mapWaypoints >= 2 then
_DrawRouteLine(mapWaypoints, currentZone)
end
end
--- Update route based on current mode
function QuestieRouteOptimizer:Update() function QuestieRouteOptimizer:Update()
local mode = Questie.db.profile.routeMode or ROUTE_MODE_OFF local mode = Questie.db.profile.routeMode or ROUTE_MODE_OFF
@@ -331,18 +255,37 @@ function QuestieRouteOptimizer:Update()
elseif mode == ROUTE_MODE_SINGLE_QUEST then elseif mode == ROUTE_MODE_SINGLE_QUEST then
local questId = QuestieTracker:GetSelectedQuest() local questId = QuestieTracker:GetSelectedQuest()
if questId then if questId then
self:DrawQuestRoute(questId) local coords = _GetQuestObjectiveCoords(questId)
if coords and #coords >= 2 then
self:DrawRoute(coords)
else
self:ClearRoutes()
end
else
self:ClearRoutes()
end
elseif mode == ROUTE_MODE_ALL_TRACKED or mode == ROUTE_MODE_TSP_APPROXIMATION then
local coords = _GetAllTrackedQuestCoords()
if coords and #coords >= 2 then
self:DrawRoute(coords)
else else
self:ClearRoutes() self:ClearRoutes()
end end
elseif mode == ROUTE_MODE_ALL_TRACKED then
self:DrawAllTrackedRoutes()
elseif mode == ROUTE_MODE_TSP_APPROXIMATION then
self:DrawTSPRoute()
end end
end end
--- Get route mode from settings --- Toggle route visibility
function QuestieRouteOptimizer:Toggle()
if routeIcons and #routeIcons > 0 then
self:ClearRoutes()
Questie.db.profile.routeMode = ROUTE_MODE_OFF
else
self:Update()
end
end
--- Get current route mode
---@return number mode
function QuestieRouteOptimizer:GetMode() function QuestieRouteOptimizer:GetMode()
return Questie.db.profile.routeMode or ROUTE_MODE_OFF return Questie.db.profile.routeMode or ROUTE_MODE_OFF
end end