Files
Questie-X/Modules/Arrow/QuestieArrow.lua
T
Xurkon 44244f504f debug(Arrow): show spawnList coords and world coords for target
- PrintTargetCoords: add world coords computed from zone coord + uiMapId
- _CollectObjective debug: show spawnList[id] zone, firstCoord, isLearned per entry
2026-05-22 20:46:29 -05:00

1261 lines
54 KiB
Lua

---@class QuestieArrow
local QuestieArrow = QuestieLoader:CreateModule("QuestieArrow")
---@type ZoneDB
local ZoneDB = QuestieLoader:ImportModule("ZoneDB")
---@type QuestieLib
local QuestieLib = QuestieLoader:ImportModule("QuestieLib")
---@type QuestieMap
local QuestieMap = QuestieLoader:ImportModule("QuestieMap")
---@type QuestieTracker
local QuestieTracker = QuestieLoader:ImportModule("QuestieTracker")
---@type QuestieDB
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
---@type QuestiePlayer
local QuestiePlayer = QuestieLoader:ImportModule("QuestiePlayer")
---@type QuestieQuest
local QuestieQuest = QuestieLoader:ImportModule("QuestieQuest")
local HBD = QuestieCompat.HBD or LibStub("HereBeDragonsQuestie-2.0")
local SharedMedia = LibStub and LibStub("LibSharedMedia-3.0", true)
local atan2 = math.atan2
local pi = math.pi
local floor = math.floor
local abs = math.abs
local max = math.max
local min = math.min
-- Single-frame arrow with SetRotation for perfectly smooth rotation.
-- Texture is a 256x256 SQUARE TGA with arrow 2x horizontally stretched to fill
-- ~70% of canvas. SQUARE is critical: SetRotation rotates UVs inside the display
-- rect, so non-square textures distort at every diagonal angle. No SetTexCoord.
local ARROW_DISPLAY_WIDTH = 160
local ARROW_DISPLAY_HEIGHT = 160
local UPDATE_THROTTLE_SECONDS = 0.05
local RECALC_NEAREST_SECONDS = 1.0
local TRACKER_REFRESH_THROTTLE_SECONDS = 0.5
---@type Frame?
local arrowFrame = nil
---@type Frame?
local driverFrame = nil
-- Current auto-tracked targets sorted by distance
local sortedTargets = {}
local hasManualTarget = false
-- Shared context written by UpdateNearestTargets, read by hoisted helpers.
-- Avoids closure allocation on every call.
local _arrow_playerX, _arrow_playerY, _arrow_playerInstance
local _arrow_usingAutoLogic, _arrow_playerZoneId, _arrow_playerUiMapId
local _arrow_quest -- current quest being processed by the hoisted helpers
local lastPopulateByQuestId = {}
local function _IsArrowEnabled()
if not Questie or not Questie.db or not Questie.db.profile then
return true
end
return Questie.db.profile.arrowEnabled ~= false
end
local function _GetArrowScale()
if not Questie or not Questie.db or not Questie.db.profile then
return 1
end
return Questie.db.profile.arrowScale or 1
end
local function _GetArrowAlpha()
if not Questie or not Questie.db or not Questie.db.profile then
return 1.0
end
return Questie.db.profile.arrowAlpha or 1.0
end
local function _SetArrowScale(scale)
if not Questie or not Questie.db or not Questie.db.profile then
return
end
Questie.db.profile.arrowScale = scale
end
local function _GetProfilePosition()
if not Questie or not Questie.db or not Questie.db.profile then
return nil
end
return Questie.db.profile.arrowPosition
end
local function _SaveProfilePosition(point, relativePoint, x, y)
if not Questie or not Questie.db or not Questie.db.profile then
return
end
Questie.db.profile.arrowPosition = {
point = point,
relativePoint = relativePoint,
x = x,
y = y,
}
end
local function modulo(val, by)
return val - floor(val / by) * by
end
local function GetColorGradient(perc)
if perc <= 0.5 then
return 1, perc * 2, 0
else
return 2 - perc * 2, 1, 0
end
end
local function ResolveIconTexture(icon)
if not icon then
return nil
end
if type(icon) == "string" then
return icon
end
if type(icon) == "number" then
if Questie and Questie.usedIcons then
return Questie.usedIcons[icon]
end
return nil
end
return nil
end
local function _ResolveArrowUiMapId(uiMapId)
-- Ghost map 946 has no real coordinate data; redirect to Eversong (1941).
-- NOTE: 1241 (Sunstrider Isle) is no longer redirected here - zoneDB now
-- maps areaId 3431 → uiMapId 1241, and player is on uiMapId 1241, so 1241
-- stays as 1241 for correct world-coord computation via its own bounds.
if uiMapId == 946 then
return 1941
end
return uiMapId
end
local function _GetSunstriderPlayerMapPosition(debugArrow)
-- NEVER use the calibrated branch here. It returns Eversong-wide normalized coords
-- (~0.60, 0.44) which are in a completely different coordinate space from
-- Sunstrider-local target coords (~0.38, 0.21). Mixing them causes bogus distances.
-- Always obtain Sunstrider-local coords via the 1241 map lookup, then convert
-- through HBD using Eversong bounds (1941) to get real comparable world coords.
local mapX2, mapY2
if QuestieCompat and QuestieCompat.C_Map and QuestieCompat.C_Map.GetPlayerMapPosition then
local mapPos = QuestieCompat.C_Map.GetPlayerMapPosition(1241, "player")
if type(mapPos) == "table" then
mapX2, mapY2 = mapPos.x, mapPos.y
if debugArrow then
print(string.format("Sunstrider helper: explicit 1241 lookup -> mapX=%.4f mapY=%.4f", mapX2 or 0, mapY2 or 0))
end
if mapX2 and mapY2 and mapX2 > 0 and mapY2 > 0 then
return mapX2, mapY2
end
end
end
if QuestieCompat and QuestieCompat.GetCurrentPlayerPosition then
local resolvedUiMapId, compatX, compatY = QuestieCompat.GetCurrentPlayerPosition()
mapX2, mapY2 = compatX, compatY
if debugArrow then
local has1241 = QuestieCompat and QuestieCompat.UiMapData and QuestieCompat.UiMapData[1241] and true or false
print(string.format("Sunstrider helper: compat current-zone uiMapId=%s mapX=%.4f mapY=%.4f hasUiMap1241=%s",
tostring(resolvedUiMapId), mapX2 or 0, mapY2 or 0, tostring(has1241)))
end
if mapX2 and mapY2 and mapX2 > 0 and mapY2 > 0 then
return mapX2, mapY2
end
end
mapX2, mapY2 = GetPlayerMapPosition("player")
return mapX2, mapY2
end
local function _ApplyOutline(fontString)
if not fontString or not fontString.GetFont or not fontString.SetFont then
return
end
local font, size, flags = fontString:GetFont()
if not font then
return
end
flags = flags or ""
if not string.find(flags, "OUTLINE", 1, true) then
if flags ~= "" then
flags = flags .. ",OUTLINE"
else
flags = "OUTLINE"
end
end
fontString:SetFont(font, size, flags)
end
local function EnsureArrowFrame()
if arrowFrame then
-- Apply current scale and alpha settings even if frame already exists
arrowFrame:SetScale(_GetArrowScale())
arrowFrame:SetAlpha(_GetArrowAlpha())
return
end
arrowFrame = CreateFrame("Frame", "QuestieArrowFrame", UIParent)
local pos = _GetProfilePosition()
if pos and pos.point and pos.relativePoint and pos.x and pos.y then
arrowFrame:SetPoint(pos.point, UIParent, pos.relativePoint, pos.x, pos.y)
else
arrowFrame:SetPoint("CENTER", 0, -100)
end
-- Store whether we should use saved position or default
arrowFrame._useDefaultPosition = not (pos and pos.point)
-- Make room for arrow (square) plus icon and text below
arrowFrame:SetWidth(ARROW_DISPLAY_WIDTH)
arrowFrame:SetHeight(ARROW_DISPLAY_HEIGHT + 60)
arrowFrame:SetScale(_GetArrowScale())
arrowFrame:SetClampedToScreen(true)
arrowFrame:SetMovable(true)
arrowFrame:EnableMouse(true)
arrowFrame:EnableMouseWheel(true)
arrowFrame:RegisterForDrag("LeftButton")
arrowFrame:SetScript("OnDragStart", function(self)
if IsShiftKeyDown() then
self:StartMoving()
end
end)
arrowFrame:SetScript("OnDragStop", function(self)
self:StopMovingOrSizing()
local point, _, relativePoint, x, y = self:GetPoint(1)
if point and relativePoint and x and y then
_SaveProfilePosition(point, relativePoint, x, y)
end
end)
arrowFrame:SetScript("OnMouseWheel", function(self, delta)
if not IsShiftKeyDown() then
return
end
local scale = _GetArrowScale() or 1
local step = 0.05
if delta and delta > 0 then
scale = scale + step
else
scale = scale - step
end
if scale < 0.5 then scale = 0.5 end
if scale > 2.0 then scale = 2.0 end
_SetArrowScale(scale)
self:SetScale(scale)
end)
-- Single arrow texture with SetRotation for smooth rotation
arrowFrame.arrow = arrowFrame:CreateTexture(nil, "MEDIUM")
arrowFrame.arrow:SetTexture(QuestieLib.AddonPath .. "Icons\\arrow.tga")
arrowFrame.arrow:SetWidth(ARROW_DISPLAY_WIDTH)
arrowFrame.arrow:SetHeight(ARROW_DISPLAY_HEIGHT)
arrowFrame.arrow:SetPoint("CENTER", arrowFrame, "CENTER", 0, 0)
arrowFrame.arrow:SetRotation(0) -- 0 = pointing up (north)
-- Quest icon texture at bottom (pfQuest style)
arrowFrame.icon = arrowFrame:CreateTexture(nil, "OVERLAY")
arrowFrame.icon:SetWidth(28)
arrowFrame.icon:SetHeight(28)
arrowFrame.icon:SetPoint("BOTTOM", arrowFrame.arrow, "BOTTOM", 0, 0)
arrowFrame.title = arrowFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
arrowFrame.title:SetPoint("TOP", arrowFrame.icon, "BOTTOM", 0, -2)
arrowFrame.title:SetJustifyH("CENTER")
_ApplyOutline(arrowFrame.title)
arrowFrame.distance = arrowFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
arrowFrame.distance:SetPoint("TOP", arrowFrame.title, "BOTTOM", 0, -2)
arrowFrame.distance:SetJustifyH("CENTER")
arrowFrame.distance:SetTextColor(1, 1, 1, 1)
_ApplyOutline(arrowFrame.distance)
arrowFrame._lastUpdate = 0
arrowFrame._lastRecalc = 0
arrowFrame._lastTarget = nil
-- Right-click to clear manual target and resume auto-tracking
arrowFrame:EnableMouse(true)
arrowFrame:SetScript("OnMouseUp", function(self, button)
if button == "RightButton" then
hasManualTarget = false
sortedTargets = {}
QuestieArrow:Refresh()
end
end)
arrowFrame:SetScript("OnUpdate", function(self)
local now = GetTime()
local target = sortedTargets[1]
if not target then
self:Hide()
return
end
if not self:IsShown() then
self:Show()
end
if (self._lastUpdate or 0) + UPDATE_THROTTLE_SECONDS > now then
return
end
self._lastUpdate = now
local debugArrow = Questie and Questie.db and Questie.db.profile and Questie.db.profile.debugArrow
local target = sortedTargets[1]
-- -----------------------------------------------------------------
-- Get fresh player world position every frame for smooth arrow
-- rotation. UnitPosition updates every frame via HBD, which is
-- essential — cached values (updated only every 1s) make the
-- arrow rotate with the character since only GetPlayerFacing()
-- changes per frame when position is stale.
-- On Sunstrider, HBD may return Eastern Kingdoms coords. We
-- detect this and override with the cached corrected position.
-- -----------------------------------------------------------------
local playerX, playerY, playerInstance = HBD:GetPlayerWorldPosition()
if not playerX or not playerY or not playerInstance then
self.distance:SetText("Distance: --")
return
end
-- On Sunstrider, HBD returns Eastern Kingdoms continent coords
-- which are outside Eversong bounds. Instead of replacing fresh
-- per-frame coords with stale 1-second cache, compute fresh
-- Sunstrider-local coords via C_Map on EVERY frame so that
-- both position AND facing update per-frame.
-- PITFALL: zoneId can be 3431 (Eversong) while player is on uiMap 1241 (Sunstrider).
-- Check both zoneId and cached uiMapId for robust detection.
local _curZoneId = QuestiePlayer:GetCurrentZoneId()
local _sunOnUpdate = (_curZoneId == 3430 or _curZoneId == 3431
or _arrow_playerUiMapId == 1241)
if _sunOnUpdate then
local EXMIN, EXMAX, EYMIN, EYMAX = -2000, 3200, 5300, 8700
if playerX < EXMIN or playerX > EXMAX or playerY < EYMIN or playerY > EYMAX then
-- Fresh per-frame computation instead of stale cache
local mapX, mapY = _GetSunstriderPlayerMapPosition(debugArrow)
if mapX and mapY and mapX > 0 and mapY > 0 then
local wX, wY, wInst = HBD:GetWorldCoordinatesFromZone(mapX, mapY, 1241) -- use Ascension-calibrated 1241 bounds
if wX and wY then
if debugArrow then
print(string.format("OnUpdate: Sunstrider fresh HBD(%.0f,%.0f) -> computed(%.0f,%.0f)",
playerX, playerY, wX, wY))
end
playerX, playerY, playerInstance = wX, wY, wInst or playerInstance
elseif _arrow_playerX and _arrow_playerY then
-- Last resort: stale cache only if fresh calc fails
if debugArrow then
print(string.format("OnUpdate: Sunstrider fresh calc FAILED, using stale cache(%.0f,%.0f)",
_arrow_playerX, _arrow_playerY))
end
playerX, playerY, playerInstance = _arrow_playerX, _arrow_playerY, _arrow_playerInstance
end
elseif _arrow_playerX and _arrow_playerY then
-- _GetSunstriderPlayerMapPosition failed, stale cache fallback
if debugArrow then
print(string.format("OnUpdate: Sunstrider map pos failed, using stale cache(%.0f,%.0f)",
_arrow_playerX, _arrow_playerY))
end
playerX, playerY, playerInstance = _arrow_playerX, _arrow_playerY, _arrow_playerInstance
end
end
end
-- Convert target spawn coords to world coordinates.
-- Resolve uiMapId again here as a safety net for targets that may
-- have raw custom map IDs (1241) bypassing PopulateTargets resolution.
local targetUiMapId = _ResolveArrowUiMapId(target.uiMapId)
-- On Sunstrider, convert target through Ascension-calibrated 1241 bounds
-- so target world coords match player world coords (both through 1241).
-- PITFALL: zoneId can be 3431 while player is on uiMap 1241.
if _sunOnUpdate and targetUiMapId == 1941 then
targetUiMapId = 1241
end
local targetX, targetY, targetInstance = HBD:GetWorldCoordinatesFromZone(target.x / 100.0, target.y / 100.0, targetUiMapId)
if not targetX or not targetY or not targetInstance then
self.distance:SetText("Distance: --")
return
end
if targetInstance ~= playerInstance then
self:Hide()
return
end
-- Arrow direction from pure world-coordinate math.
-- HBD world coords: X decreases going EAST (more negative = more west).
-- Y increases going NORTH (larger = more north).
-- GetPlayerFacing: 0=North, π/2=East, π=South, 3π/2=West (CW from N).
-- SetRotation(r): rotates texture CW (positive = clockwise).
-- Arrow image tip is at TOP of file → points UP at SetRotation(0).
--
-- dx = targetX - playerX:
-- targetX > playerX (numerically) → target LESS negative → target EAST
-- So dx>0 = target EAST of player
-- dy = targetY - playerY:
-- targetY > playerY → target MORE north
-- So dy>0 = target NORTH of player
--
-- Bearing CW from North: atan2(dx, dy)
-- N: dx=0, dy>0 → atan2(0,+) = 0
-- E: dx>0, dy=0 → atan2(+,0) = π/2
-- S: dx=0, dy<0 → atan2(0,-) = π
-- W: dx<0, dy=0 → atan2(-,0) = -π/2 → 3π/2
--
-- Screen direction relative to facing:
-- relative = bearing - facing (0 = target ahead)
--
-- SetRotation wants CW; relative is CW: use it directly.
-- SetRotation(relative) → arrow points at target on screen.
local dx = targetX - playerX
local dy = targetY - playerY
local bearing = atan2(dx, dy)
if bearing < 0 then bearing = bearing + (pi * 2) end
local facing = GetPlayerFacing and GetPlayerFacing() or 0
local relative = bearing - facing
if relative < 0 then relative = relative + (pi * 2) end
local rotAngle = relative -- CW rotation for SetRotation
if debugArrow then
print(string.format("QuestieArrow OnUpdate: target=%s pX=%.1f pY=%.1f tX=%.1f tY=%.1f dx=%.1f dy=%.1f bearing=%.2f facing=%.2f relative=%.2f rotAng=%.2f inst=%s",
tostring(target.title), playerX, playerY, targetX, targetY, dx, dy, bearing, facing, relative, rotAngle, tostring(playerInstance)))
end
-- Calculate distance and alpha
local dist = HBD:GetWorldDistance(targetInstance, playerX, playerY, targetX, targetY)
if dist then
local area = 1
local alpha = dist - area
alpha = alpha > 1 and 1 or alpha
alpha = alpha < 0.5 and 0.5 or alpha
local texalpha = (1 - alpha) * 2
texalpha = texalpha > 1 and 1 or texalpha
texalpha = texalpha < 0 and 0 or texalpha
self.arrow:SetRotation(rotAngle)
self.arrow:SetVertexColor(1, 1, 1)
self.arrow:SetAlpha(alpha)
local distText = string.format("%.1f", dist)
self.distance:SetText("Distance: " .. distText)
end
-- Update title and icon when target changes
if target ~= self._lastTarget then
self._lastTarget = target
local title = target.title or ""
if target.questLevel then
title = "[" .. target.questLevel .. "] " .. title
end
self.title:SetText(Questie:Colorize(title, "gold"))
if target.iconPath then
self.icon:SetTexture(target.iconPath)
self.icon:Show()
else
self.icon:Hide()
end
end
end)
arrowFrame:Hide()
end
local function EnsureDriverFrame()
if driverFrame then
return
end
driverFrame = CreateFrame("Frame", "QuestieArrowDriverFrame", UIParent)
driverFrame:Show()
driverFrame._lastRecalc = 0
driverFrame:SetScript("OnUpdate", function(self)
local now = GetTime()
if (self._lastRecalc or 0) + RECALC_NEAREST_SECONDS < now then
self._lastRecalc = now
if not _IsArrowEnabled() then
if arrowFrame then
arrowFrame:Hide()
end
return
end
QuestieArrow:Refresh()
end
end)
end
-- ---------------------------------------------------------------------------
-- Hoisted helpers for UpdateNearestTargets.
-- These were previously closures recreated on every call; now they are
-- module-level functions that read shared upvalue state set each cycle.
-- ---------------------------------------------------------------------------
local function _HasMissingCompletedFlag(list)
if not list then return false end
for _, obj in pairs(list) do
if obj and obj.Completed == nil then
return true
end
end
return false
end
local function _GetCompleteIconType(quest)
local iconType = Questie.ICON_TYPE_COMPLETE
if QuestieDB and QuestieDB.IsActiveEventQuest and QuestieDB.IsActiveEventQuest(quest.Id) then
iconType = Questie.ICON_TYPE_EVENTQUEST_COMPLETE
elseif QuestieDB and QuestieDB.IsPvPQuest and QuestieDB.IsPvPQuest(quest.Id) then
iconType = Questie.ICON_TYPE_PVPQUEST_COMPLETE
elseif quest.IsRepeatable then
iconType = Questie.ICON_TYPE_REPEATABLE_COMPLETE
end
return iconType
end
local function _CollectFinisherSpawns(finisher, quest)
if not finisher then return end
local pX, pY, pInst = _arrow_playerX, _arrow_playerY, _arrow_playerInstance
local autoLogic, pZone, pMap = _arrow_usingAutoLogic, _arrow_playerZoneId, _arrow_playerUiMapId
-- On Sunstrider, force target conversion through 1241 bounds to match player coords
local sunOverride = (pMap == 1241)
local iconPath = ResolveIconTexture(_GetCompleteIconType(quest))
if finisher.spawns then
for finisherZone, spawns in pairs(finisher.spawns) do
if finisherZone and spawns then
for _, coords in ipairs(spawns) do
if coords and coords[1] and coords[2] then
if coords[1] == -1 or coords[2] == -1 then
local dungeonLocation = ZoneDB:GetDungeonLocation(finisherZone)
if dungeonLocation then
for _, value in ipairs(dungeonLocation) do
local zone = value[1]
local x = value[2]
local y = value[3]
-- Zone filtering disabled (zone ID vs area ID mismatch)
if true then
local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone)
if uiMapId and x and y then
local resolvedUiMapId = _ResolveArrowUiMapId(uiMapId)
if sunOverride and resolvedUiMapId == 1941 then resolvedUiMapId = 1241 end
local tX, tY, tInst = HBD:GetWorldCoordinatesFromZone(x / 100.0, y / 100.0, resolvedUiMapId)
if tX and tY and tInst then
local dist = HBD:GetWorldDistance(tInst, pX, pY, tX, tY)
if dist then
if tInst ~= pInst then dist = 500000 + dist * 100 end
table.insert(sortedTargets, {
x = x, y = y, uiMapId = resolvedUiMapId, title = quest.name, questLevel = quest.level, iconPath = iconPath, distance = dist,
})
end
end
end
end
end
end
else
-- Zone filtering disabled (same zone ID vs area ID mismatch issue)
if true then
local x = coords[1]
local y = coords[2]
local uiMapId = ZoneDB:GetUiMapIdByAreaId(finisherZone)
if uiMapId then
local resolvedUiMapId = _ResolveArrowUiMapId(uiMapId)
if sunOverride and resolvedUiMapId == 1941 then resolvedUiMapId = 1241 end
local tX, tY, tInst = HBD:GetWorldCoordinatesFromZone(x / 100.0, y / 100.0, resolvedUiMapId)
if tX and tY and tInst then
local dist = HBD:GetWorldDistance(tInst, pX, pY, tX, tY)
if dist then
if tInst ~= pInst then dist = 500000 + dist * 100 end
table.insert(sortedTargets, {
x = x, y = y, uiMapId = resolvedUiMapId, title = quest.name, questLevel = quest.level, iconPath = iconPath, distance = dist,
})
end
end
end
end
end
end
end
end
end
end
if finisher.waypoints then
for zone, waypoints in pairs(finisher.waypoints) do
-- Zone filtering disabled (same zone ID vs area ID mismatch issue)
if true then
if waypoints and waypoints[1] and waypoints[1][1] and waypoints[1][1][1] then
local x = waypoints[1][1][1]
local y = waypoints[1][1][2]
local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone)
if uiMapId and x and y then
local resolvedUiMapId = _ResolveArrowUiMapId(uiMapId)
if sunOverride and resolvedUiMapId == 1941 then resolvedUiMapId = 1241 end
local tX, tY, tInst = HBD:GetWorldCoordinatesFromZone(x / 100.0, y / 100.0, resolvedUiMapId)
if tX and tY and tInst then
local dist = HBD:GetWorldDistance(tInst, pX, pY, tX, tY)
if dist then
if tInst ~= pInst then dist = 500000 + dist * 100 end
table.insert(sortedTargets, {
x = x, y = y, uiMapId = resolvedUiMapId, title = quest.name, questLevel = quest.level, iconPath = iconPath, distance = dist,
})
end
end
end
end
end
end
end
end
local function _CollectObjective(objective, quest)
if not objective or not objective.spawnList then
-- spawnList can be nil if _TryInvalidateObjective cleared it (QuestieLearner
-- learned new data) but the rebuild via UpdateQuest hasn't happened yet, or if
-- stale quest.isComplete prevented PopulateObjective from rebuilding it.
-- Proactively trigger UpdateQuest to rebuild the spawnList for this quest.
if quest and quest.Id and QuestieQuest and QuestieQuest.UpdateQuest then
local dbComplete = QuestieDB.IsComplete(quest.Id)
-- Only request rebuild if quest is NOT complete in the DB
if dbComplete ~= 1 and not quest.isComplete then
local now = GetTime()
local last = lastPopulateByQuestId[quest.Id] or 0
-- Throttle rebuilds to every 5 seconds per quest
if (last + 5.0) < now then
lastPopulateByQuestId[quest.Id] = now
Questie:Debug(Questie.DEBUG_DEVELOP, "[Arrow] _CollectObjective: spawnList nil for quest", quest.Id, "- triggering UpdateQuest rebuild")
QuestieQuest:UpdateQuest(quest.Id)
end
end
end
if debugCollect then
print(string.format(" _CollectObjective SKIP: obj=%s spawnList=%s (quest=%s)",
tostring(objective), objective and tostring(objective.spawnList) or "nil", quest and tostring(quest.name) or "?"))
end
return
end
if QuestieQuest.ShouldHideObjective(objective) then return end
if objective.Completed == true or objective.Completed == 1 then
if debugCollect then
print(string.format(" _CollectObjective SKIP: Completed=%s (quest=%s)", tostring(objective.Completed), quest and tostring(quest.name) or "?"))
end
return
end
if objective.Needed and objective.Collected
and type(objective.Needed) == "number" and type(objective.Collected) == "number"
and objective.Collected >= objective.Needed then
return
end
local pX, pY, pInst = _arrow_playerX, _arrow_playerY, _arrow_playerInstance
local autoLogic, pZone, pMap = _arrow_usingAutoLogic, _arrow_playerZoneId, _arrow_playerUiMapId
-- On Sunstrider, force target conversion through 1241 bounds to match player coords
local sunOverride = (pMap == 1241)
local debugCollect = Questie and Questie.db and Questie.db.profile and Questie.db.profile.debugArrow
if debugCollect then
print(string.format(" _CollectObjective: spawnList=%s", objective.spawnList and "yes" or "nil"))
for id, spawnData in pairs(objective.spawnList) do
if spawnData and spawnData.Spawns then
for zone, spawns in pairs(spawnData.Spawns) do
if spawns and #spawns > 0 then
print(string.format(" spawnList[%d]: zone=%s firstCoord=(%.2f,%.2f) isLearned=%s",
id, tostring(zone), spawns[1][1], spawns[1][2], tostring(spawnData.isLearned)))
end
end
end
end
end
if not objective.spawnList then return end
for _, spawnData in pairs(objective.spawnList) do
if debugCollect then
print(string.format(" spawnData=%s Spawns=%s", spawnData and "yes" or "nil", spawnData and spawnData.Spawns and "yes" or "nil"))
end
if spawnData and spawnData.Spawns then
for zone, spawns in pairs(spawnData.Spawns) do
-- Zone filtering is disabled in auto mode because zone IDs and area IDs
-- are different systems that don't directly compare. The distance
-- calculation handles instance mismatches.
local zoneFiltered = false -- Disabled: autoLogic and zone ~= pZone and zone ~= pMap
if debugCollect then
print(string.format(" zone=%s filtered=%s (pZone=%s pMap=%s)", tostring(zone), tostring(zoneFiltered), tostring(pZone), tostring(pMap)))
end
if not zoneFiltered then
for _, spawn in pairs(spawns) do
local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone)
if debugCollect then
print(string.format(" spawn=(%.1f,%.1f) uiMapId=%s", spawn[1], spawn[2], tostring(uiMapId)))
end
if uiMapId then
local resolvedUiMapId = _ResolveArrowUiMapId(uiMapId)
if sunOverride and resolvedUiMapId == 1941 then resolvedUiMapId = 1241 end
local tX, tY, tInst = HBD:GetWorldCoordinatesFromZone(spawn[1] / 100.0, spawn[2] / 100.0, resolvedUiMapId)
if tX and tY and tInst then
local dist = HBD:GetWorldDistance(tInst, pX, pY, tX, tY)
if dist then
if tInst ~= pInst then dist = 500000 + dist * 100 end
if debugCollect then
print(string.format(" ADDED dist=%.0f", dist))
end
table.insert(sortedTargets, {
x = spawn[1], y = spawn[2], uiMapId = resolvedUiMapId,
title = quest.name, questLevel = quest.level,
iconPath = ResolveIconTexture(objective.Icon) or ResolveIconTexture(spawnData and spawnData.Icon),
distance = dist,
})
end
end
end
end
end
end
end
end
end
-- Gather all objectives from tracked quests and sort by distance
function QuestieArrow:UpdateNearestTargets()
-- Don't override manual targets with auto-updates
if hasManualTarget then
return
end
sortedTargets = {}
if not Questie.db or not Questie.db.char then
return
end
local debugArrow = Questie and Questie.db and Questie.db.profile and Questie.db.profile.debugArrow
-- Detect Sunstrider Isle first (before calling HBD) since HBD:GetPlayerWorldPosition()
-- returns non-nil but WRONG coords on Sunstrider (Eastern Kingdoms position instead of
-- Sunstrider's actual position), causing the fallback below to never fire.
local zoneId = QuestiePlayer:GetCurrentZoneId()
local pUiMapId = QuestiePlayer:GetCurrentUiMapId()
-- On Sunstrider Isle (areaId 3430, uiMapId 1241), HBD:GetPlayerWorldPosition() returns
-- Eastern Kingdoms world coords because that's the continent HBD thinks the player is on.
-- We must use C_Map.GetPlayerMapPosition(1241) + HBD:GetWorldCoordinatesFromZone(..., 1941).
-- NOTE: GetCurrentUiMapId() returns 1241 on Sunstrider Isle (not 946 or 1941).
-- PITFALL: zoneId can be 3431 (Eversong) while player is on uiMap 1241 (Sunstrider).
local useSunstriderFix = (zoneId == 3430 or zoneId == 3431 or pUiMapId == 1241)
-- Get player position — always try HBD's direct method first (works when map is OPEN).
-- If that returns nil (map closed or Sunstrider), fall back to C_Map.GetPlayerMapPosition +
-- HBD:GetWorldCoordinatesFromZone which works regardless of map open/closed state.
local playerX, playerY, playerInstance = HBD:GetPlayerWorldPosition()
if not playerX or not playerY or not playerInstance then
-- Fallback: get map-relative position then convert to world coords via HBD.
-- IMPORTANT: never use 946/947 (world/cosmic maps) — they have no world coord data.
-- If GetCurrentUiMapId returns a world map, fall back to ZoneDB from the actual zone.
if not pUiMapId or pUiMapId == 946 or pUiMapId == 947 or pUiMapId == 0 then
zoneId = QuestiePlayer:GetCurrentZoneId() or select(7, GetInstanceInfo())
if debugArrow then
print(string.format("UpdateNearestTargets: pUiMapId=%s (invalid), looking up via zoneId=%s", tostring(pUiMapId), tostring(zoneId)))
end
if zoneId then
pUiMapId = ZoneDB:GetUiMapIdByAreaId(zoneId)
end
end
-- Additional safeguard: if pUiMapId is still a world/cosmic map, force lookup from zone
if not pUiMapId or pUiMapId == 946 or pUiMapId == 947 or pUiMapId == 0 then
zoneId = QuestiePlayer:GetCurrentZoneId()
if zoneId and zoneId ~= 0 then
pUiMapId = ZoneDB:GetUiMapIdByAreaId(zoneId)
if debugArrow then
print(string.format("UpdateNearestTargets: forced pUiMapId=%s via zoneId=%s", tostring(pUiMapId), tostring(zoneId)))
end
end
end
pUiMapId = pUiMapId or 0
-- On Sunstrider Isle (zoneId 3430), C_Map.GetPlayerMapPosition returns Sunstrider
-- map-space coords. We must look up with the actual Sunstrider uiMapId (1241) and
-- then convert through Eversong's 1941 bounds to get correct world coords.
local lookupUiMapId = pUiMapId
if zoneId == 3430 or zoneId == 3431 or pUiMapId == 1241 then
lookupUiMapId = 1241 -- always use Sunstrider's real uiMapId for C_Map
end
if debugArrow then
print(string.format("UpdateNearestTargets: trying C_Map with lookupUiMapId=%s zoneId=%s", tostring(lookupUiMapId), tostring(zoneId)))
end
-- On Sunstrider, raw GetPlayerMapPosition(1941, "player") returns 0/0 when the map is
-- closed because the client map context is still the ghost/current map. Use the helper
-- above to obtain current-zone coords, then convert them through Eversong's 1941 bounds.
local mapX, mapY = _GetSunstriderPlayerMapPosition(debugArrow)
if debugArrow then
print(string.format("UpdateNearestTargets: _GetSunstriderPlayerMapPosition() -> mapX=%.4f mapY=%.4f", mapX or -1, mapY or -1))
end
if mapX and mapY and mapX > 0 and mapY > 0 then
if useSunstriderFix then
-- Sunstrider: use 1941 (Eversong) for coordinate conversion so that
-- player world coords are in the same space as target world coords.
-- Targets always use 1941 (via ZoneDB:GetUiMapIdByAreaId(3430)→1941),
-- so the player must also use 1941 for consistent distance/direction.
-- Ascension-calibrated 1241 bounds now match Eversong world space.
playerX, playerY, playerInstance = HBD:GetWorldCoordinatesFromZone(mapX, mapY, 1241)
else
-- Normal zone: convert the player's map coords through the ACTUAL zone's
-- uiMapId bounds. This was hardcoded to 1941 in the original Sunstrider fix,
-- which broke every zone except Eversong.
playerX, playerY, playerInstance = HBD:GetWorldCoordinatesFromZone(mapX, mapY, lookupUiMapId)
end
if debugArrow then
print(string.format("UpdateNearestTargets: HBD via lookupUiMapId=%s mapX=%.4f mapY=%.4f -> worldX=%.4f worldY=%.4f",
tostring(lookupUiMapId), mapX, mapY, playerX or 0, playerY or 0))
end
playerInstance = playerInstance or 0
end
end
-- Sunstrider override: HBD:GetPlayerWorldPosition() may return incorrect
-- (Eastern Kingdoms offset) coords on Sunstrider Isle. Only override if
-- the HBD coords appear wrong — specifically, if they fall outside the
-- Eversong bounding box. Eversong world bounds: X ∈ [-1825, 3100],
-- Y ∈ [5358, 8642]. If HBD coords are outside this range, they're EK coords.
-- NOTE: _GetSunstriderPlayerMapPosition returns 1241-local normalized coords.
-- We convert through 1941 (Eversong) so that player world coords share the
-- same coordinate space as targets (which always use uiMapId 1941 via ZoneDB).
if useSunstriderFix and playerX and playerY then
-- Eversong bounding box in world coordinates (with some margin).
-- A player on Sunstrider/Eversong should be within these bounds.
local EVERSENG_XMIN, EVERSENG_XMAX = -2000, 3200
local EVERSENG_YMIN, EVERSENG_YMAX = 5300, 8700
local coordsOutsideEversong = (playerX < EVERSENG_XMIN or playerX > EVERSENG_XMAX
or playerY < EVERSENG_YMIN or playerY > EVERSENG_YMAX)
if debugArrow then
print(string.format("UpdateNearestTargets: Sunstrider check outsideEv=%s px=%.0f py=%.0f bounds=[%d..%d,%d..%d]",
tostring(coordsOutsideEversong), playerX, playerY,
EVERSENG_XMIN, EVERSENG_XMAX, EVERSENG_YMIN, EVERSENG_YMAX))
end
if coordsOutsideEversong then
local mapX, mapY = _GetSunstriderPlayerMapPosition(debugArrow)
if debugArrow then
print(string.format("UpdateNearestTargets: Sunstrider mapPos mapX=%.4f mapY=%.4f", mapX or -1, mapY or -1))
end
if mapX and mapY and mapX > 0 and mapY > 0 then
-- Use Ascension-calibrated 1241 bounds for consistent world space.
local wX, wY, wInst = HBD:GetWorldCoordinatesFromZone(mapX, mapY, 1241)
if debugArrow then
print(string.format("UpdateNearestTargets: Sunstrider HBD1941 wX=%s wY=%s wInst=%s", tostring(wX), tostring(wY), tostring(wInst)))
end
if wX and wY then
if debugArrow then
print(string.format("UpdateNearestTargets: Sunstrider override HBD(%.4f,%.4f) -> HBD1941(%.4f,%.4f)", playerX, playerY, wX, wY))
end
playerX, playerY, playerInstance = wX, wY, wInst or 0
else
-- HBD 1941 conversion failed — no fallback available.
if debugArrow then
print("UpdateNearestTargets: Sunstrider HBD1941 conversion FAILED")
end
end
end
end
end
if debugArrow then
print(string.format("UpdateNearestTargets: HBD.GetPlayerWorldPosition() = x=%.4f y=%.4f inst=%s", playerX or 0, playerY or 0, tostring(playerInstance)))
end
if not playerX or not playerY or not playerInstance then
if debugArrow then
print("UpdateNearestTargets: player position unavailable, returning early")
end
return
end
playerInstance = playerInstance or 0
if debugArrow then
print(string.format("UpdateNearestTargets: playerX=%.4f playerY=%.4f playerInstance=%s",
playerX, playerY, tostring(playerInstance)))
end
local tracked = Questie.db.char.TrackedQuests or {}
local hasTracked = next(tracked) ~= nil
-- Auto mode logic: If autoTrack is on OR NOTHING is tracked
local usingAutoLogic = Questie.db.profile.autoTrackQuests or not hasTracked
local playerZoneId = QuestiePlayer:GetCurrentZoneId()
-- Get a valid uiMapId for the player — needed for _CollectObjective zone filtering.
-- Use QuestiePlayer which calls C_Map.GetBestMapForUnit — if that returns 947 (wrong)
-- fall back to ZoneDB from the player's actual zone (areaId).
local playerUiMapId = QuestiePlayer:GetCurrentUiMapId()
if not playerUiMapId or playerUiMapId == 947 then
local zoneId = playerZoneId or select(7, GetInstanceInfo())
if zoneId then
playerUiMapId = ZoneDB:GetUiMapIdByAreaId(zoneId) or playerUiMapId
end
end
playerUiMapId = playerUiMapId or 0
-- Publish context for hoisted helper functions (avoids closure allocation every call)
_arrow_playerX, _arrow_playerY, _arrow_playerInstance = playerX, playerY, playerInstance
_arrow_usingAutoLogic = usingAutoLogic
_arrow_playerZoneId, _arrow_playerUiMapId = playerZoneId, playerUiMapId
local function _CollectQuestTargets(quest)
if not quest then return end
local debugCollect = Questie and Questie.db and Questie.db.profile and Questie.db.profile.debugArrow
-- Periodic quest state verification: call PopulateQuestLogInfo when
-- (a) objectives/completion flags are missing (original logic), OR
-- (b) the quest hasn't been populated in the last 30 seconds
-- (c) quest.isComplete is stale (does not match DB)
-- This ensures the arrow always has accurate completion data, catching
-- cases where events didn't fire or stale flags survived a reload.
if QuestieQuest and QuestieQuest.PopulateQuestLogInfo and quest.Id then
local needsPopulate = false
if not quest.Objectives and not quest.SpecialObjectives then
needsPopulate = true
elseif _HasMissingCompletedFlag(quest.Objectives) or _HasMissingCompletedFlag(quest.SpecialObjectives) then
needsPopulate = true
end
-- Periodic force-refresh: if more than 30 seconds since last populate, re-sync
-- quest state from live quest log. This catches stale isComplete/WasComplete
-- that survived event-driven updates (e.g., Ascension missing QUEST_LOG_UPDATE).
local now = GetTime()
local last = lastPopulateByQuestId[quest.Id] or 0
if (last + 30.0) < now then
needsPopulate = true
end
if needsPopulate then
if (last + 2.0) < now then
lastPopulateByQuestId[quest.Id] = now
QuestieQuest:PopulateQuestLogInfo(quest)
end
end
end
local dbComplete = QuestieDB.IsComplete(quest.Id)
-- Defensive: if quest.isComplete is stale from a prior complete-then-abandon, and
-- QuestLogCache says the quest is NOT complete (0 or nil), clear the stale flag so
-- objective pins are drawn. AcceptQuest normally resets this, but this guards
-- against edge cases where AcceptQuest's reset didn't fire.
if quest.isComplete and dbComplete ~= 1 then
quest.isComplete = nil
end
local isComplete = quest.isComplete or (dbComplete == 1)
if debugCollect then
local objCount = quest.Objectives and #quest.Objectives or 0
print(string.format(" _CollectQuestTargets: %s isComplete=%s (quest.isComplete=%s dbComplete=%s) objCount=%d hasObjectives=%s hasSpecialObjectives=%s hasFinisher=%s",
tostring(quest.name), tostring(isComplete),
tostring(quest.isComplete), tostring(dbComplete),
objCount,
tostring(quest.Objectives ~= nil),
tostring(quest.SpecialObjectives ~= nil),
tostring(quest.Finisher ~= nil)))
end
-- _GetCompleteIconType, _CollectFinisherSpawns, _CollectObjective are hoisted
-- to module level above; no closures are created here.
-- Main Logic Route for this quest target
if isComplete then
if quest.Finisher and quest.Finisher.Id and quest.Finisher.Type then
local finisher
if quest.Finisher.Type == "monster" and QuestieDB and QuestieDB.GetNPC then
finisher = QuestieDB:GetNPC(quest.Finisher.Id)
elseif quest.Finisher.Type == "object" and QuestieDB and QuestieDB.GetObject then
finisher = QuestieDB:GetObject(quest.Finisher.Id)
end
_CollectFinisherSpawns(finisher, quest)
end
-- If the quest is complete, do not add normal objectives to the arrow!
return
end
if quest.Objectives then
if debugCollect then print(string.format(" Collecting %d objectives", #quest.Objectives)) end
for _, objective in pairs(quest.Objectives) do
_CollectObjective(objective, quest)
end
else
if debugCollect then print(" No Objectives") end
end
if quest.SpecialObjectives then
if debugCollect then print(string.format(" Collecting %d special objectives", #quest.SpecialObjectives)) end
for _, objective in pairs(quest.SpecialObjectives) do
_CollectObjective(objective, quest)
end
end
end
if QuestiePlayer and QuestiePlayer.currentQuestlog then
local debugCollect = Questie and Questie.db and Questie.db.profile and Questie.db.profile.debugArrow
for questId, quest in pairs(QuestiePlayer.currentQuestlog) do
if debugCollect then
print(string.format("Processing questId=%d type=%s", questId, type(quest)))
end
if type(quest) == "number" then
if QuestieDB and QuestieDB.GetQuest then
quest = QuestieDB.GetQuest(questId)
end
end
if type(quest) == "table" then
local shouldTrack = false
if usingAutoLogic then
if not Questie.db.char.AutoUntrackedQuests or not Questie.db.char.AutoUntrackedQuests[questId] then
shouldTrack = true
end
else
if Questie.db.char.TrackedQuests and Questie.db.char.TrackedQuests[questId] then
shouldTrack = true
end
end
if debugCollect then
print(string.format(" questId=%d shouldTrack=%s usingAutoLogic=%s", questId, tostring(shouldTrack), tostring(usingAutoLogic)))
end
if shouldTrack then
if debugCollect then
print(string.format(" Calling _CollectQuestTargets for %s", tostring(quest.name)))
end
_CollectQuestTargets(quest)
end
end
end
end
-- Sort by distance
table.sort(sortedTargets, function(a, b) return a.distance < b.distance end)
end
function QuestieArrow:Refresh()
if not _IsArrowEnabled() then
if arrowFrame then
arrowFrame:Hide()
end
return
end
QuestieArrow:UpdateNearestTargets()
EnsureArrowFrame()
local alpha = _GetArrowAlpha()
local scale = _GetArrowScale()
if arrowFrame then
arrowFrame:SetAlpha(alpha)
arrowFrame:SetScale(scale)
end
if sortedTargets[1] then
arrowFrame:Show()
else
arrowFrame:Hide()
end
end
-- Manual target setting (called from tracker TomTom bind)
---@param title string
---@param zoneOrUiMapId number
---@param x number
---@param y number
function QuestieArrow:SetTarget(title, zoneOrUiMapId, x, y)
if not _IsArrowEnabled() then
return
end
-- For manual targets, insert at front of sorted list
local uiMapId = ZoneDB:GetUiMapIdByAreaId(zoneOrUiMapId) or zoneOrUiMapId
hasManualTarget = true
sortedTargets = { {
x = x,
y = y,
uiMapId = uiMapId,
title = title,
distance = 0, -- Manual targets always go first
} }
EnsureArrowFrame()
arrowFrame:SetAlpha(_GetArrowAlpha())
arrowFrame:Show()
end
function QuestieArrow:ClearTarget()
hasManualTarget = false
sortedTargets = {}
if arrowFrame then
arrowFrame:Hide()
end
end
function QuestieArrow:ResetPosition()
Questie.db.profile.arrowPosition = nil
-- Ensure frame exists, then reset to default center position
EnsureArrowFrame()
if arrowFrame then
arrowFrame:ClearAllPoints()
arrowFrame:SetPoint("CENTER", UIParent, "CENTER", 0, -100)
arrowFrame._useDefaultPosition = true
end
end
function QuestieArrow:ApplyScale()
EnsureArrowFrame()
if arrowFrame then
arrowFrame:SetScale(_GetArrowScale())
end
end
function QuestieArrow:ApplyAlpha()
EnsureArrowFrame()
if arrowFrame then
arrowFrame:SetAlpha(_GetArrowAlpha())
end
end
function QuestieArrow:UpdateSettings()
EnsureArrowFrame()
if arrowFrame then
arrowFrame:SetScale(_GetArrowScale())
arrowFrame:SetAlpha(_GetArrowAlpha())
QuestieArrow:UpdateFont()
end
end
function QuestieArrow:UpdateFont()
if not arrowFrame then return end
local fontSize = Questie.db.profile.arrowFontSize or 10
local fontName = Questie.db.profile.arrowFont or "Friz Quadrata TT"
local fontFace = (SharedMedia and SharedMedia.Fetch and SharedMedia:Fetch("font", fontName)) or fontName
if arrowFrame.title then
arrowFrame.title:SetFont(fontFace, fontSize, "OUTLINE")
end
if arrowFrame.distance then
arrowFrame.distance:SetFont(fontFace, fontSize - 2, "OUTLINE")
end
end
function QuestieArrow:Initialize()
EnsureArrowFrame()
EnsureDriverFrame()
-- Refresh immediately on tracker updates (quest progress, objective completion, etc.)
if QuestieTracker and QuestieTracker.Update and hooksecurefunc then
local lastTrackerRefresh = 0
hooksecurefunc(QuestieTracker, "Update", function()
if hasManualTarget or not _IsArrowEnabled() then
return
end
local now = GetTime()
if (lastTrackerRefresh + TRACKER_REFRESH_THROTTLE_SECONDS) > now then
return
end
lastTrackerRefresh = now
QuestieArrow:Refresh()
end)
end
QuestieArrow:Refresh()
end
-- Debug function to show current arrow target coordinates
function QuestieArrow:PrintTargetCoords()
if not sortedTargets or not sortedTargets[1] then
print("Questie Arrow: No target currently set!")
return
end
local target = sortedTargets[1]
print("Questie Arrow Target:")
print(" Quest: " .. tostring(target.title))
print(" Level: " .. tostring(target.questLevel))
print(" Zone Coords: " .. string.format("%.1f, %.1f", target.x, target.y))
print(" UI Map ID: " .. tostring(target.uiMapId))
print(" Distance: " .. string.format("%.0f", target.distance))
-- Debug: show world coords computed from zone coord + uiMapId
local tX, tY, tInst = HBD:GetWorldCoordinatesFromZone(target.x / 100.0, target.y / 100.0, target.uiMapId)
if tX and tY then
print(" World Coords: " .. string.format("%.1f, %.1f", tX, tY))
end
end
function QuestieArrow:DebugPrint()
print("=== Questie Arrow Debug ===")
print("sortedTargets count: " .. tostring(#sortedTargets))
print("hasManualTarget: " .. tostring(hasManualTarget))
print("_arrow_usingAutoLogic: " .. tostring(_arrow_usingAutoLogic))
print("_arrow_playerX: " .. tostring(_arrow_playerX))
print("_arrow_playerY: " .. tostring(_arrow_playerY))
print("_arrow_playerZoneId: " .. tostring(_arrow_playerZoneId))
print("_arrow_playerUiMapId: " .. tostring(_arrow_playerUiMapId))
if Questie and Questie.db and Questie.db.profile then
print("autoTrackQuests: " .. tostring(Questie.db.profile.autoTrackQuests))
end
if Questie and Questie.db and Questie.db.char then
local tracked = Questie.db.char.TrackedQuests or {}
local autoUntracked = Questie.db.char.AutoUntrackedQuests or {}
local trackedCount = 0
for _ in pairs(tracked) do trackedCount = trackedCount + 1 end
local autoUntrackedCount = 0
for _ in pairs(autoUntracked) do autoUntrackedCount = autoUntrackedCount + 1 end
print("TrackedQuests count: " .. tostring(trackedCount))
print("AutoUntrackedQuests count: " .. tostring(autoUntrackedCount))
end
if QuestiePlayer and QuestiePlayer.currentQuestlog then
local count = 0
for _ in pairs(QuestiePlayer.currentQuestlog) do count = count + 1 end
print("currentQuestlog count: " .. tostring(count))
end
if sortedTargets and #sortedTargets > 0 then
print("First 3 targets:")
for i = 1, math.min(3, #sortedTargets) do
local t = sortedTargets[i]
print(string.format(" [%d] %s (%.1f, %.1f) dist=%.0f", i, tostring(t.title), t.x, t.y, t.distance))
end
end
print("========================")
end
-- Also expose sortedTargets for external access
function QuestieArrow:GetTargets()
return sortedTargets
end
-- Hook into Refresh to show debug info when arrow updates
local _OriginalRefresh = QuestieArrow.Refresh
QuestieArrow.Refresh = function(self, ...)
_OriginalRefresh(self, ...)
if Questie and Questie.db and Questie.db.profile and Questie.db.profile.debugArrow then
if sortedTargets and sortedTargets[1] then
QuestieArrow:PrintTargetCoords()
end
end
end