diff --git a/Modules/QuestieSlash.lua b/Modules/QuestieSlash.lua index 35b84a2..e71076b 100644 --- a/Modules/QuestieSlash.lua +++ b/Modules/QuestieSlash.lua @@ -21,6 +21,8 @@ local QuestieDB = QuestieLoader:ImportModule("QuestieDB") local l10n = QuestieLoader:ImportModule("l10n") ---@type QuestieCombatQueue local QuestieCombatQueue = QuestieLoader:ImportModule("QuestieCombatQueue") +---@type QuestieRouteOptimizer +local QuestieRouteOptimizer = QuestieLoader:ImportModule("QuestieRouteOptimizer") 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 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 route [off/single/all/tsp] - " .. l10n("Toggle quest route optimization. off=disable, single=single quest, all=all tracked, tsp=TSP approximation"), "yellow")); return; end @@ -218,5 +221,30 @@ function QuestieSlash.HandleCommands(input) return 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")); end diff --git a/Modules/Tracker/QuestieRouteOptimizer.lua b/Modules/Tracker/QuestieRouteOptimizer.lua index f4c301a..e44a45a 100644 --- a/Modules/Tracker/QuestieRouteOptimizer.lua +++ b/Modules/Tracker/QuestieRouteOptimizer.lua @@ -10,25 +10,21 @@ local QuestieDB = QuestieLoader:ImportModule("QuestieDB") 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") +---@type QuestieLib +local QuestieLib = QuestieLoader:ImportModule("QuestieLib") +---@type l10n +local l10n = QuestieLoader:ImportModule("l10n") ------------------------- --Compat ------------------------- -local GetTime = GetTime local pairs = pairs local ipairs = ipairs local tinsert = table.insert -local type = type ------------------------- --Route optimization modes @@ -41,8 +37,14 @@ local ROUTE_MODE_TSP_APPROXIMATION = 4 ------------------------- --State ------------------------- -local routeFrames = {} -local currentRouteMode = ROUTE_MODE_OFF +local routeIcons = {} +local routeLines = {} + +------------------------- +--Constants +------------------------- +local ROUTE_COLOR = {0.2, 0.8, 1, 0.8} +local WAYPOINT_ICON = "Interface\\WorldMap\\WorldMapPartyIcon" --- Calculate distance between two points ---@param x1 number @@ -54,9 +56,21 @@ local function _GetDistance(x1, y1, x2, y2) return math.sqrt((x2 - x1) ^ 2 + (y2 - y1) ^ 2) 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 ----@param coordinates table ----@return table +---@param coordinates table +---@return table local function _NearestNeighborTSP(coordinates) if #coordinates <= 1 then return coordinates @@ -96,28 +110,32 @@ local function _NearestNeighborTSP(coordinates) return route end ---- Get spawn coordinates for a single quest +--- Get spawn coordinates for objectives in a quest ---@param questId number ----@return table? -local function _GetQuestSpawnCoordinates(questId) +---@return table? +local function _GetQuestObjectiveCoords(questId) local quest = QuestieDB.GetQuest(questId) if not quest then return nil end local coordinates = {} + local currentMapId = GetCurrentMapAreaID() - ---@param spawnData table + ---@param spawns table? ---@param zoneId number - local function AddSpawns(spawnData, zoneId) - if spawnData and spawnData.Spawns then - for _, spawn in pairs(spawnData.Spawns) do + local function AddCoords(spawns, zoneId) + if not spawns then return end + for _, spawn in pairs(spawns) do + if type(spawn) == "table" then 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 - }) + if type(coord) == "table" and coord[1] and coord[2] then + local x, y = coord[1], coord[2] + if x > 0 and x <= 100 and y > 0 and y <= 100 then + tinsert(coordinates, { + x = x / 100, + y = y / 100, + zone = zoneId + }) + end end end end @@ -128,34 +146,7 @@ local function _GetQuestSpawnCoordinates(questId) 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) + AddCoords(spawnData, zoneId) end end end @@ -165,15 +156,15 @@ local function _GetQuestSpawnCoordinates(questId) end --- Get spawn coordinates for all tracked quests ----@return table? -local function _GetAllTrackedQuestsCoordinates() +---@return table? +local function _GetAllTrackedQuestCoords() 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) + local questCoords = _GetQuestObjectiveCoords(questId) if questCoords then for _, coord in pairs(questCoords) do tinsert(coordinates, coord) @@ -184,145 +175,78 @@ local function _GetAllTrackedQuestsCoordinates() return coordinates end ---- Clear all route frames +--- Clear all route visuals function QuestieRouteOptimizer:ClearRoutes() - for _, frame in pairs(routeFrames) do - if frame and frame:Hide then - frame:Hide() + for _, icon in pairs(routeIcons) do + if icon and icon.Hide then + icon:Hide() end end - routeFrames = {} + routeIcons = {} + + for _, line in pairs(routeLines) do + if line and line.Hide then + line:Hide() + end + end + routeLines = {} 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 +--- Draw a route line between waypoints +---@param waypoints table ---@param zoneId number -function QuestieRouteOptimizer:_DrawZoneRoute(waypoints, zoneId) +local function _DrawRouteLine(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) + local icon = CreateFrame("Button", "QuestieRouteIcon" .. #routeIcons, UIParent) + icon:SetWidth(1) + icon:SetHeight(1) + icon:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + icon:SetFrameLevel(1) + icon:Show() + tinsert(routeIcons, icon) + local lineFrames = QuestieFramePool:CreateWaypoints(icon, waypoints, 2, ROUTE_COLOR, zoneId) for _, lineFrame in ipairs(lineFrames) do - tinsert(routeFrames, lineFrame) + lineFrame:Show() + tinsert(routeLines, lineFrame) end end ---- Update route display based on current mode +--- Draw optimized route for given coordinates +---@param coordinates table +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() local mode = Questie.db.profile.routeMode or ROUTE_MODE_OFF @@ -331,18 +255,37 @@ function QuestieRouteOptimizer:Update() elseif mode == ROUTE_MODE_SINGLE_QUEST then local questId = QuestieTracker:GetSelectedQuest() 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 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 +--- 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() return Questie.db.profile.routeMode or ROUTE_MODE_OFF end