fix(arrow): Sunstrider zoneId 3431, rotation CW, collection distance, native map pins
- zoneId detection: accept 3430 OR 3431 OR uiMapId 1241 (4 locations) - SetRotation is CW-positive: rotAngle=relative (was -relative) - Collection functions: override target->1241 when player on Sunstrider - NPC 15281: spawn zone 1241 (not 3430) for correct coord space - _ResolveMapUiMapId: removed 1241->1941 redirect - zoneDB: added areaIdToUiMapId[1241]=1241 - Updated CHANGELOG, README, docs
This commit is contained in:
+286
-282
@@ -26,12 +26,12 @@ local abs = math.abs
|
||||
local max = math.max
|
||||
local min = math.min
|
||||
|
||||
local ARROW_SHEET_SIZE = 512
|
||||
local ARROW_CELL_W = 56
|
||||
local ARROW_CELL_H = 42
|
||||
local ARROW_SHEET_COLS = 9
|
||||
local ARROW_SHEET_ROWS = 12
|
||||
local ARROW_TOTAL_CELLS = ARROW_SHEET_COLS * ARROW_SHEET_ROWS
|
||||
-- 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
|
||||
@@ -49,10 +49,10 @@ 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_playerCalibratedX, _arrow_playerCalibratedY, _arrow_playerCalibratedGroup
|
||||
local _arrow_usingAutoLogic, _arrow_playerZoneId, _arrow_playerUiMapId
|
||||
local _arrow_quest -- current quest being processed by the hoisted helpers
|
||||
|
||||
|
||||
local lastPopulateByQuestId = {}
|
||||
|
||||
local function _IsArrowEnabled()
|
||||
@@ -134,36 +134,24 @@ local function ResolveIconTexture(icon)
|
||||
end
|
||||
|
||||
local function _ResolveArrowUiMapId(uiMapId)
|
||||
-- Sunstrider Isle (1241) and its ghost map (946) both resolve to Eversong Woods (1941)
|
||||
-- for arrow calculations. This normalizes both player and target uiMapIds so the
|
||||
-- same-map branch fires and zone-relative coord math works consistently.
|
||||
-- NOTE: _ResolveMapUiMapId in QuestieMap.lua also redirects 1241→1941 because
|
||||
-- on Ascension, map 1241 shares Eversong's coordinate space. Zone 3430 data
|
||||
-- now maps to uiMapId 1941 via GetUiMapIdByAreaId(3430)=1941, so quest items
|
||||
-- render on the Eversong map and appear on Sunstrider via ZONE_REDIRECT.
|
||||
if uiMapId == 1241 or uiMapId == 946 then
|
||||
-- Ghost map 946 has no real coordinate data; redirect to Eversong (1941).
|
||||
-- Ascension custom map 1241 (Sunstrider Isle) must also redirect to 1941
|
||||
-- so target world coords match player world coords (both in Eversong space).
|
||||
if uiMapId == 946 or uiMapId == 1241 then
|
||||
return 1941
|
||||
end
|
||||
return uiMapId
|
||||
end
|
||||
|
||||
local function _GetSunstriderPlayerMapPosition(debugArrow)
|
||||
local worldX, worldY, _, mapX, mapY, group = QuestieCompat.GetCalibratedPlayerPosition(_arrow_playerUiMapId, _arrow_playerZoneId, "player")
|
||||
if group and mapX and mapY then
|
||||
if debugArrow then
|
||||
print(string.format("Sunstrider helper: calibrated primary=%s -> mapX=%.4f mapY=%.4f worldX=%.1f worldY=%.1f",
|
||||
tostring(group.primaryUiMapId), mapX, mapY, worldX or 0, worldY or 0))
|
||||
end
|
||||
return mapX, mapY
|
||||
end
|
||||
|
||||
-- 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
|
||||
-- For local player map coords on Sunstrider, ask for the actual child map (1241).
|
||||
-- Asking for parent 1941 returns parent-relative coords (~60/44) which recreates
|
||||
-- the classic 433-yard / backwards-arrow bug when compared against Sunstrider-local
|
||||
-- target coords (~38/21).
|
||||
local mapPos = QuestieCompat.C_Map.GetPlayerMapPosition(1241, "player")
|
||||
if type(mapPos) == "table" then
|
||||
mapX2, mapY2 = mapPos.x, mapPos.y
|
||||
@@ -193,6 +181,8 @@ local function _GetSunstriderPlayerMapPosition(debugArrow)
|
||||
return mapX2, mapY2
|
||||
end
|
||||
|
||||
|
||||
|
||||
local function _ApplyOutline(fontString)
|
||||
if not fontString or not fontString.GetFont or not fontString.SetFont then
|
||||
return
|
||||
@@ -235,9 +225,9 @@ local function EnsureArrowFrame()
|
||||
-- Store whether we should use saved position or default
|
||||
arrowFrame._useDefaultPosition = not (pos and pos.point)
|
||||
|
||||
-- Make room for the objective icon below the arrow (no overlap)
|
||||
arrowFrame:SetWidth(56)
|
||||
arrowFrame:SetHeight(64)
|
||||
-- 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)
|
||||
@@ -278,20 +268,19 @@ local function EnsureArrowFrame()
|
||||
self:SetScale(scale)
|
||||
end)
|
||||
|
||||
-- Arrow sprite sheet texture (108 cells: 9 columns, 12 rows)
|
||||
-- Single arrow texture with SetRotation for smooth rotation
|
||||
arrowFrame.arrow = arrowFrame:CreateTexture(nil, "MEDIUM")
|
||||
arrowFrame.arrow:SetTexture(QuestieLib.AddonPath .. "Icons\\arrow.tga")
|
||||
-- Render at native cell size; use frame scaling if you want it larger.
|
||||
arrowFrame.arrow:SetWidth(ARROW_CELL_W)
|
||||
arrowFrame.arrow:SetHeight(ARROW_CELL_H)
|
||||
arrowFrame.arrow:SetPoint("TOP", arrowFrame, "TOP", 0, 0)
|
||||
arrowFrame.arrow:SetTexCoord(0, 0.109375, 0, 0.08203125) -- First cell
|
||||
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, -20)
|
||||
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)
|
||||
@@ -334,213 +323,131 @@ arrowFrame:SetScript("OnUpdate", function(self)
|
||||
if (self._lastUpdate or 0) + UPDATE_THROTTLE_SECONDS > now then
|
||||
return
|
||||
end
|
||||
self._lastUpdate = now
|
||||
self._lastUpdate = now
|
||||
|
||||
-- Persistent debug: print every frame so we can see what OnUpdate sees
|
||||
local debugArrow = Questie and Questie.db and Questie.db.profile and Questie.db.profile.debugArrow
|
||||
if debugArrow then
|
||||
print(string.format("QuestieArrow OnUpdate: frameShown=%s target=%s pX=%s pY=%s pInst=%s _playerUiMapId=%s targetUiMapId=%s",
|
||||
tostring(self:IsShown()), tostring(target and target.title),
|
||||
tostring(_arrow_playerX), tostring(_arrow_playerY), tostring(_arrow_playerInstance),
|
||||
tostring(_arrow_playerUiMapId), tostring(target and target.uiMapId)))
|
||||
|
||||
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
|
||||
|
||||
local pX, pY, pInst = _arrow_playerX, _arrow_playerY, _arrow_playerInstance
|
||||
if not pX or not pY or not pInst then
|
||||
-- Fallback to HBD if upvalues aren't set yet
|
||||
pX, pY, pInst = HBD:GetPlayerWorldPosition()
|
||||
-- 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
|
||||
if debugArrow then
|
||||
local hbPX, hbPY, hbPInst = HBD:GetPlayerWorldPosition()
|
||||
print(string.format("DEBUG PLAYER COMPARE: UnitPos pX=%.1f pY=%.1f | HBD pX=%.1f pY=%.1f | inst=%s",
|
||||
pX or 0, pY or 0, hbPX or 0, hbPY or 0, tostring(hbPInst)))
|
||||
print(string.format("DEBUG MAP RELATIVE: pX=%.4f pY=%.4f (from _arrow_playerX/Y, UnitPosition world coords)", pX or 0, pY or 0))
|
||||
print(string.format("DEBUG OnUpdate: rawUiMapId=%s resolved=%s targetUiMapId=%s", tostring(_arrow_playerUiMapId), tostring(playerUiMapId), tostring(targetUiMapId)))
|
||||
print(string.format("DEBUG SPAWN COORDS: target worldX=%.1f worldY=%.1f rawMapX=%.2f rawMapY=%.2f",
|
||||
targetX or 0, targetY or 0, rawMapX or 0, rawMapY or 0))
|
||||
end
|
||||
if not pX or not pY or not pInst then
|
||||
local debugArrow = Questie and Questie.db and Questie.db.profile and Questie.db.profile.debugArrow
|
||||
if debugArrow then
|
||||
print(string.format("QuestieArrow OnUpdate: player position nil (x=%s y=%s inst=%s)", tostring(pX), tostring(pY), tostring(pInst)))
|
||||
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
|
||||
|
||||
-- Player's ACTUAL uiMapId (1241 on Sunstrider) vs target's uiMapId.
|
||||
-- _arrow_playerUiMapId is the real map the player is on (1241 on Sunstrider).
|
||||
-- target.uiMapId is the resolved uiMapId for the spawn (1941 for Sunstrider targets).
|
||||
-- Use the player's REAL uiMapId for the same-map check — not the resolved one.
|
||||
-- _ResolveArrowUiMapId normalizes both player and target uiMapIds to the same
|
||||
-- coordinate system: 1241→1941, 946→1941. This lets the same-map branch work
|
||||
-- on Sunstrider Isle where player is on 946 but targets resolve to 1941.
|
||||
local playerUiMapId = _ResolveArrowUiMapId(_arrow_playerUiMapId) or 0
|
||||
local targetUiMapId = _ResolveArrowUiMapId(target.uiMapId) or 0
|
||||
-- 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
|
||||
|
||||
-- Declare world coords outside the branches so they're in scope for direction calc
|
||||
local playerWorldX, playerWorldY
|
||||
|
||||
-- If target and player are on the same uiMapId, they're in the same instance.
|
||||
-- If they're on different maps, we need to compare instances via HBD.
|
||||
if playerUiMapId ~= targetUiMapId then
|
||||
-- Different maps: just skip instance check since UnitPosition gives us
|
||||
-- the real instance — if player and target instances differ, HBD will return
|
||||
-- nil for distance anyway. Fall through to direction calc with player coords.
|
||||
end
|
||||
|
||||
-- Calculate arrow direction using zone-relative coords when on same map (avoids
|
||||
-- world-coordinate mismatch on Sunstrider Isle where player is in EK world space).
|
||||
-- For cross-map, fall back to world coords via HBD.
|
||||
-- The key insight: on Sunstrider, _arrow_playerX/Y (world EK coords) and target.x/y
|
||||
-- (zone coords in Sunstrider space) are incompatible. We use C_Map to get the player's
|
||||
-- zone coords in Sunstrider space so they align with target zone coords.
|
||||
local targetX, targetY, targetInstance
|
||||
local useZoneAngle = false
|
||||
local zoneAngle = nil
|
||||
local zoneBasedDist = nil
|
||||
-- Declare worldPlayerX/Y here so they're in scope for the debug print at line 478
|
||||
-- even when the same-map branch takes the zone-relative path (useZoneAngle=true).
|
||||
local worldPlayerX, worldPlayerY
|
||||
if debugArrow then
|
||||
print(string.format("DEBUG BRANCH CHECK: playerUiMapId=%s targetUiMapId=%s sameMap=%s",
|
||||
tostring(playerUiMapId), tostring(targetUiMapId),
|
||||
tostring(playerUiMapId == targetUiMapId and playerUiMapId ~= 0)))
|
||||
end
|
||||
if playerUiMapId == targetUiMapId and playerUiMapId ~= 0 then
|
||||
-- Same uiMapId on Sunstrider/Eversong: use zone-relative coordinates for BOTH
|
||||
-- distance and direction. Mixing player UnitPosition world coords with HBD target
|
||||
-- coords recreates the classic 433-yard / backwards-arrow bug.
|
||||
local pZoneX, pZoneY = _GetSunstriderPlayerMapPosition(debugArrow)
|
||||
local playerZoneX, playerZoneY = pZoneX * 100, pZoneY * 100
|
||||
local targetZoneX, targetZoneY = target.x, target.y
|
||||
local zoneDist = sqrt((playerZoneX - targetZoneX) ^ 2 + (playerZoneY - targetZoneY) ^ 2)
|
||||
-- Eversong/Sunstrider: 100 zone-units ≈ 1353 yards (full map width from HBD bounds)
|
||||
local zoneScale = 13.53 -- yards per zone-unit
|
||||
zoneBasedDist = zoneDist * zoneScale
|
||||
|
||||
local zoneXDelta = (playerZoneX - targetZoneX) * 1.5
|
||||
local zoneYDelta = -(playerZoneY - targetZoneY)
|
||||
zoneAngle = atan2(zoneXDelta, -zoneYDelta)
|
||||
zoneAngle = zoneAngle > 0 and (pi * 2) - zoneAngle or -zoneAngle
|
||||
if zoneAngle < 0 then zoneAngle = zoneAngle + (pi * 2) end
|
||||
|
||||
targetX, targetY, targetInstance = targetZoneX, targetZoneY, 0
|
||||
useZoneAngle = true
|
||||
|
||||
if debugArrow then
|
||||
local dbg1941x, dbg1941y = HBD:GetZoneCoordinatesFromWorld(pX, pY, 1941, true)
|
||||
local dbg1241x, dbg1241y = HBD:GetZoneCoordinatesFromWorld(pX, pY, 1241, true)
|
||||
print(string.format("DEBUG SAME_MAP: zoneDist=%.4f zoneYards≈%.1f | pZone(%.2f,%.2f) tZone(%.2f,%.2f) angle=%.2f | world->1941(%.4f,%.4f) world->1241(%.4f,%.4f)",
|
||||
zoneDist, zoneBasedDist, playerZoneX, playerZoneY, targetZoneX, targetZoneY,
|
||||
zoneAngle or 0,
|
||||
(dbg1941x or -1), (dbg1941y or -1),
|
||||
(dbg1241x or -1), (dbg1241y or -1)))
|
||||
end
|
||||
else
|
||||
-- Different maps: convert target to world coords using its uiMapId (works for 1941).
|
||||
-- Player is already in world coords from UnitPosition via pX/pY.
|
||||
local tWX, tWY, tInst = HBD:GetWorldCoordinatesFromZone(target.x / 100.0, target.y / 100.0, targetUiMapId)
|
||||
if tWX and tWY then
|
||||
targetX, targetY, targetInstance = tWX, tWY, tInst or pInst or 0
|
||||
if debugArrow then
|
||||
print(string.format("DEBUG OnUpdate else-branch: tWX=%.4f tWY=%.4f tInst=%s using HBD", tWX, tWY, tostring(tInst)))
|
||||
end
|
||||
else
|
||||
-- HBD failed: fall back to zone coords (target.x, target.y) for direction only.
|
||||
-- Distance will be wrong (zone units instead of yards) but arrow will point correctly.
|
||||
targetX, targetY = target.x, target.y
|
||||
targetInstance = pInst or 0
|
||||
if debugArrow then
|
||||
print(string.format("DEBUG OnUpdate else-branch: HBD failed for target uiMapId=%s, falling back to zone coords (%.2f, %.2f)", tostring(targetUiMapId), targetX, targetY))
|
||||
end
|
||||
end
|
||||
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
|
||||
|
||||
-- If targetX is still nil at this point, bail out
|
||||
if not targetX or not targetY then
|
||||
self.distance:SetText("Distance: --")
|
||||
return
|
||||
end
|
||||
|
||||
-- Skip world direction calc when same-map; zoneAngle already computed above
|
||||
if useZoneAngle then
|
||||
-- zoneAngle already set; apply facing and proceed.
|
||||
-- Sunstrider calibrated zone-space currently resolves to the inverse heading
|
||||
-- relative to the arrow sprite sheet, so flip by 180 degrees after facing.
|
||||
angle = zoneAngle - (GetPlayerFacing and GetPlayerFacing() or 0) + pi
|
||||
if angle < 0 then angle = angle + (pi * 2) end
|
||||
if angle >= (pi * 2) then angle = angle - (pi * 2) end
|
||||
else
|
||||
-- Use world coords for direction: both player and target must be in world coordinate space.
|
||||
-- UnitPosition("player") gives world coords directly. For same-uiMapId: pX/pY from
|
||||
-- UpdateNearestTargets are world coords from UnitPosition — use directly.
|
||||
if playerWorldX then
|
||||
worldPlayerX, worldPlayerY = playerWorldX, playerWorldY
|
||||
else
|
||||
worldPlayerX, worldPlayerY = pX, pY
|
||||
end
|
||||
|
||||
if not targetX or not targetY then
|
||||
self.distance:SetText("Distance: --")
|
||||
return
|
||||
end
|
||||
|
||||
local xDelta = (worldPlayerX - targetX) * 1.5
|
||||
local yDelta = (worldPlayerY - targetY)
|
||||
angle = atan2(xDelta, -(yDelta))
|
||||
angle = angle > 0 and (pi * 2) - angle or -angle
|
||||
if angle < 0 then angle = angle + (pi * 2) end
|
||||
|
||||
angle = angle - (GetPlayerFacing and GetPlayerFacing() or 0)
|
||||
end
|
||||
|
||||
-- Calculate color gradient based on direction
|
||||
local perc = abs(((pi - abs(angle)) / pi))
|
||||
local r, g, b = GetColorGradient(perc)
|
||||
|
||||
-- Select sprite sheet cell
|
||||
local cell = modulo(floor(angle / (pi * 2) * ARROW_TOTAL_CELLS + 0.5), ARROW_TOTAL_CELLS)
|
||||
local column = modulo(cell, ARROW_SHEET_COLS)
|
||||
local row = floor(cell / ARROW_SHEET_COLS)
|
||||
local xstart = (column * ARROW_CELL_W) / ARROW_SHEET_SIZE
|
||||
local ystart = (row * ARROW_CELL_H) / ARROW_SHEET_SIZE
|
||||
local xend = ((column + 1) * ARROW_CELL_W) / ARROW_SHEET_SIZE
|
||||
local yend = ((row + 1) * ARROW_CELL_H) / ARROW_SHEET_SIZE
|
||||
|
||||
-- Avoid bleeding from neighboring cells when texture filtering is enabled.
|
||||
local padX = 0.5 / ARROW_SHEET_SIZE
|
||||
local padY = 0.5 / ARROW_SHEET_SIZE
|
||||
xstart = xstart + padX
|
||||
ystart = ystart + padY
|
||||
xend = xend - padX
|
||||
yend = yend - padY
|
||||
|
||||
-- Calculate distance and alpha
|
||||
-- Override distance with zone-based when same-map (avoids cross-world-system mismatch)
|
||||
local dist = nil
|
||||
if useZoneAngle and zoneBasedDist then
|
||||
dist = zoneBasedDist
|
||||
else
|
||||
dist = HBD:GetWorldDistance(targetInstance, worldPlayerX, worldPlayerY, targetX, targetY)
|
||||
end
|
||||
if debugArrow then
|
||||
local dbgDist = dist or 0
|
||||
local dbgPX = worldPlayerX or 0
|
||||
local dbgPY = worldPlayerY or 0
|
||||
print(string.format("DEBUG DIST: dist=%s inst=%s pX=%.1f pY=%.1f tX=%.1f tY=%.1f rawMapX=%s rawMapY=%s",
|
||||
tostring(dbgDist), tostring(targetInstance),
|
||||
dbgPX, dbgPY,
|
||||
targetX or 0, targetY or 0,
|
||||
tostring(target.x), tostring(target.y)))
|
||||
end
|
||||
-- Calculate distance and alpha
|
||||
local dist = HBD:GetWorldDistance(targetInstance, playerX, playerY, targetX, targetY)
|
||||
if dist then
|
||||
if debugArrow then
|
||||
local dbgWorldPX = worldPlayerX or targetX or 0
|
||||
local dbgWorldPY = worldPlayerY or targetY or 0
|
||||
print(string.format("QuestieArrow OnUpdate: dist=%.1f worldPlayerX=%.1f worldPlayerY=%.1f targetX=%.1f targetY=%.1f targetInst=%s title='%s' rawMapX=%s rawMapY=%s", dist, dbgWorldPX, dbgWorldPY, targetX or 0, targetY or 0, tostring(targetInstance), tostring(target.title), tostring(target.x), tostring(target.y)))
|
||||
end
|
||||
local area = 1
|
||||
local alpha = dist - area
|
||||
alpha = alpha > 1 and 1 or alpha
|
||||
@@ -550,10 +457,8 @@ local pX, pY, pInst = _arrow_playerX, _arrow_playerY, _arrow_playerInstance
|
||||
texalpha = texalpha > 1 and 1 or texalpha
|
||||
texalpha = texalpha < 0 and 0 or texalpha
|
||||
|
||||
r, g, b = r + texalpha, g + texalpha, b + texalpha
|
||||
|
||||
self.arrow:SetTexCoord(xstart, xend, ystart, yend)
|
||||
self.arrow:SetVertexColor(r, g, b)
|
||||
self.arrow:SetRotation(rotAngle)
|
||||
self.arrow:SetVertexColor(1, 1, 1)
|
||||
self.arrow:SetAlpha(alpha)
|
||||
|
||||
local distText = string.format("%.1f", dist)
|
||||
@@ -638,6 +543,8 @@ 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
|
||||
@@ -656,6 +563,7 @@ local function _CollectFinisherSpawns(finisher, quest)
|
||||
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)
|
||||
@@ -672,12 +580,13 @@ local function _CollectFinisherSpawns(finisher, quest)
|
||||
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 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)
|
||||
@@ -706,6 +615,7 @@ local function _CollectFinisherSpawns(finisher, quest)
|
||||
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)
|
||||
@@ -724,9 +634,38 @@ local function _CollectFinisherSpawns(finisher, quest)
|
||||
end
|
||||
|
||||
local function _CollectObjective(objective, quest)
|
||||
if not objective or not objective.spawnList then return end
|
||||
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 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
|
||||
@@ -734,6 +673,8 @@ local function _CollectObjective(objective, quest)
|
||||
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"))
|
||||
@@ -760,16 +701,12 @@ local function _CollectObjective(objective, quest)
|
||||
end
|
||||
if uiMapId then
|
||||
local resolvedUiMapId = _ResolveArrowUiMapId(uiMapId)
|
||||
local tX, tY, tInst, calibratedTargetGroup = QuestieCompat.GetCalibratedWorldCoordinatesFromZone(spawn[1] / 100.0, spawn[2] / 100.0, resolvedUiMapId, zone)
|
||||
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
|
||||
if calibratedTargetGroup and _arrow_playerCalibratedGroup and _arrow_playerCalibratedX and _arrow_playerCalibratedY then
|
||||
dist = sqrt((_arrow_playerCalibratedX - tX) ^ 2 + (_arrow_playerCalibratedY - tY) ^ 2)
|
||||
else
|
||||
dist = HBD:GetWorldDistance(tInst, pX, pY, tX, tY)
|
||||
end
|
||||
local dist = HBD:GetWorldDistance(tInst, pX, pY, tX, tY)
|
||||
if dist then
|
||||
if (not calibratedTargetGroup) and tInst ~= pInst then dist = 500000 + dist * 100 end
|
||||
if tInst ~= pInst then dist = 500000 + dist * 100 end
|
||||
if debugCollect then
|
||||
print(string.format(" ADDED dist=%.0f", dist))
|
||||
end
|
||||
@@ -813,22 +750,14 @@ sortedTargets = {}
|
||||
-- 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 946 (ghost map) on Sunstrider, not 1241 or 1941,
|
||||
-- so we check zoneId == 3430 as the primary indicator.
|
||||
local useSunstriderFix = (zoneId == 3430)
|
||||
-- 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 from the calibrated transform layer first for broken/custom maps.
|
||||
local playerX, playerY, playerInstance, calibratedMapX, calibratedMapY, calibratedGroup = QuestieCompat.GetCalibratedPlayerPosition(pUiMapId, zoneId, "player")
|
||||
|
||||
-- Get player position — first try HBD's direct method (works when map is OPEN).
|
||||
-- If that returns nil (map closed), fall back to C_Map.GetPlayerMapPosition +
|
||||
-- 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.
|
||||
-- Also skip HBD directly on Sunstrider since it returns wrong coords.
|
||||
if useSunstriderFix and calibratedGroup then
|
||||
-- already resolved via calibrated transform layer
|
||||
elseif not playerX or not playerY or not playerInstance then
|
||||
playerX, playerY, playerInstance = HBD:GetPlayerWorldPosition()
|
||||
end
|
||||
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.
|
||||
@@ -858,7 +787,7 @@ sortedTargets = {}
|
||||
-- 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 then
|
||||
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
|
||||
@@ -873,11 +802,18 @@ sortedTargets = {}
|
||||
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 calibratedGroup then
|
||||
playerX, playerY, playerInstance = QuestieCompat.GetCalibratedWorldCoordinatesFromZone(mapX, mapY, lookupUiMapId, zoneId)
|
||||
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
|
||||
local worldUiMapId = 1941 -- always use Eversong bounds for world coord conversion
|
||||
playerX, playerY, playerInstance = HBD:GetWorldCoordinatesFromZone(mapX, mapY, worldUiMapId)
|
||||
-- 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",
|
||||
@@ -886,15 +822,60 @@ sortedTargets = {}
|
||||
playerInstance = playerInstance or 0
|
||||
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)))
|
||||
-- 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 not playerX or not playerY or not playerInstance then
|
||||
if coordsOutsideEversong then
|
||||
local mapX, mapY = _GetSunstriderPlayerMapPosition(debugArrow)
|
||||
if debugArrow then
|
||||
print("UpdateNearestTargets: player position unavailable, returning early")
|
||||
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
|
||||
return
|
||||
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
|
||||
|
||||
@@ -923,17 +904,22 @@ sortedTargets = {}
|
||||
|
||||
-- Publish context for hoisted helper functions (avoids closure allocation every call)
|
||||
_arrow_playerX, _arrow_playerY, _arrow_playerInstance = playerX, playerY, playerInstance
|
||||
_arrow_playerCalibratedX, _arrow_playerCalibratedY, _arrow_playerCalibratedGroup = playerX, playerY, calibratedGroup
|
||||
_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
|
||||
|
||||
-- Avoid spamming QuestieQuest:PopulateQuestLogInfo (it can trigger marker rebuilds and flicker).
|
||||
-- Only populate when objective completion flags are missing, and throttle per quest id.
|
||||
-- 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
|
||||
@@ -941,9 +927,17 @@ sortedTargets = {}
|
||||
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
|
||||
local now = GetTime()
|
||||
local last = lastPopulateByQuestId[quest.Id] or 0
|
||||
if (last + 2.0) < now then
|
||||
lastPopulateByQuestId[quest.Id] = now
|
||||
QuestieQuest:PopulateQuestLogInfo(quest)
|
||||
@@ -951,12 +945,22 @@ sortedTargets = {}
|
||||
end
|
||||
end
|
||||
|
||||
local isComplete = quest.isComplete or (QuestieDB.IsComplete(quest.Id) == 1)
|
||||
if isComplete then quest.isComplete = true 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
|
||||
print(string.format(" _CollectQuestTargets: %s isComplete=%s hasObjectives=%s hasSpecialObjectives=%s hasFinisher=%s",
|
||||
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)))
|
||||
|
||||
@@ -58,18 +58,12 @@ local fadeLogicTimerShown
|
||||
local fadeLogicCoroutine
|
||||
|
||||
local function _ResolveMapUiMapId(uiMapId, x, y)
|
||||
-- Ascension zone mapping: On Ascension, Sunstrider Isle (uiMapId 1241) shares
|
||||
-- Eversong Woods' (1941) coordinate space. Zone 3430 (Eversong Woods) data
|
||||
-- now correctly maps to uiMapId 1941 via GetUiMapIdByAreaId(3430)=1941.
|
||||
-- These Eversong-map pins appear on the Sunstrider sub-map (1241) via
|
||||
-- ZONE_REDIRECT visibility in HBD.lua.
|
||||
--
|
||||
-- Redirect any remaining map-1241 pins to 1941. On Ascension, map 1241
|
||||
-- does not have its own independent coordinate space — it IS Eversong's
|
||||
-- space. Pins destined for 1241 must use 1941's bounds for correct rendering.
|
||||
if uiMapId == 1241 then
|
||||
-- Ghost map 946 has no real coordinate data; redirect to Eversong (1941).
|
||||
if uiMapId == 946 then
|
||||
return 1941
|
||||
end
|
||||
-- Map 1241 (Sunstrider Isle) now has its own Ascension-calibrated bounds
|
||||
-- and pins on 1241 render correctly on the Sunstrider sub-map. No redirect.
|
||||
return uiMapId
|
||||
end
|
||||
|
||||
@@ -678,7 +672,7 @@ end
|
||||
local closestStarter = {}
|
||||
function QuestieMap:FindClosestStarter()
|
||||
local playerX, playerY, _ = HBD:GetPlayerWorldPosition();
|
||||
local playerZone = HBD:GetPlayerWorldPosition();
|
||||
local playerZone = QuestiePlayer:GetCurrentUiMapId();
|
||||
for questId in pairs(QuestiePlayer.currentQuestlog) do
|
||||
if (not closestStarter[questId]) then
|
||||
local quest = QuestieDB.GetQuest(questId);
|
||||
|
||||
@@ -58,6 +58,14 @@ local deletedQuestItem = false
|
||||
local _bagUpdateDebounceTimer = nil
|
||||
local _bagUpdateFollowUpTimer = nil
|
||||
|
||||
-- Periodic quest state verification timer.
|
||||
-- Ascension server events (QUEST_LOG_UPDATE, UNIT_QUEST_LOG_CHANGED) can be
|
||||
-- unreliable or delayed, causing stale quest.isComplete / quest.WasComplete flags
|
||||
-- to persist across reload/reaccept cycles. This timer forces a full quest log
|
||||
-- reconciliation every 30 seconds so pins and arrows stay accurate.
|
||||
local _periodicRefreshTimer = nil
|
||||
local PERIODIC_REFRESH_SECONDS = 30
|
||||
|
||||
--- Registers all events that are required for questing (accepting, removing, objective updates, ...)
|
||||
function QuestEventHandler:RegisterEvents()
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[Quest Event] RegisterEvents")
|
||||
@@ -77,6 +85,18 @@ function QuestEventHandler:RegisterEvents()
|
||||
eventFrame:RegisterEvent("CHAT_MSG_COMBAT_FACTION_CHANGE")
|
||||
eventFrame:SetScript("OnEvent", _QuestEventHandler.OnEvent)
|
||||
|
||||
-- Start periodic quest state verification timer.
|
||||
-- On Ascension, QUEST_LOG_UPDATE events can be unreliable. This timer forces
|
||||
-- a full reconciliation every 30 seconds, catching stale isComplete/WasComplete
|
||||
-- flags and ensuring objective spawnLists stay populated for active quests.
|
||||
if not _periodicRefreshTimer then
|
||||
_periodicRefreshTimer = C_Timer.NewTicker(PERIODIC_REFRESH_SECONDS, function()
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[Quest Event] Periodic refresh: forcing full quest log scan")
|
||||
doFullQuestLogScan = true
|
||||
_QuestEventHandler:QuestLogUpdate()
|
||||
end)
|
||||
end
|
||||
|
||||
-- StaticPopup dialog hooks. Deleteing Quest items do not always trigger a Quest Log Update.
|
||||
hooksecurefunc("StaticPopup_Show", function(...)
|
||||
-- Hook StaticPopup_Show. If we find the "DELETE_ITEM" dialog, check for Quest Items and notify the player.
|
||||
@@ -230,6 +250,20 @@ function _QuestEventHandler:QuestAccepted(questLogIndex, questId)
|
||||
end)
|
||||
end
|
||||
|
||||
-- If the quest was already in questLog as QUEST_TURNED_IN (e.g. Ebonhold Call Board repeatable
|
||||
-- quests that vanish without a proper QUEST_REMOVED event), clean up before re-accepting so the
|
||||
-- tracker doesn't show it as already complete.
|
||||
-- NOTE: Must check BEFORE wiping questLog[questId] below, otherwise the state check is always false.
|
||||
local wasTurnedIn = questLog[questId] and questLog[questId].state == QUEST_LOG_STATES.QUEST_TURNED_IN
|
||||
if wasTurnedIn then
|
||||
Questie:Debug(Questie.DEBUG_INFO, "Quest:", questId, "re-accepted after auto-complete, clearing stale state")
|
||||
QuestLogCache.RemoveQuest(questId)
|
||||
QuestieQuest:CompleteQuest(questId) -- clears per-quest data
|
||||
QuestieJourney:CompleteQuest(questId)
|
||||
QuestieAnnounce:CompletedQuest(questId)
|
||||
QuestieTracker:RemoveQuest(questId)
|
||||
end
|
||||
|
||||
questLog[questId] = {}
|
||||
|
||||
-- Timed quests do not need a full Quest Log Update.
|
||||
@@ -241,19 +275,6 @@ function _QuestEventHandler:QuestAccepted(questLogIndex, questId)
|
||||
skipNextUQLCEvent = true
|
||||
end
|
||||
|
||||
-- If the quest was already in questLog as QUEST_TURNED_IN (e.g. Ebonhold Call Board repeatable
|
||||
-- quests that vanish without a proper QUEST_REMOVED event), clean up before re-accepting so the
|
||||
-- tracker doesn't show it as already complete.
|
||||
if questLog[questId] and questLog[questId].state == QUEST_LOG_STATES.QUEST_TURNED_IN then
|
||||
Questie:Debug(Questie.DEBUG_INFO, "Quest:", questId, "re-accepted after auto-complete, clearing stale state")
|
||||
QuestLogCache.RemoveQuest(questId)
|
||||
QuestieQuest:CompleteQuest(questId) -- clears per-quest data
|
||||
QuestieJourney:CompleteQuest(questId)
|
||||
QuestieAnnounce:CompletedQuest(questId)
|
||||
QuestieTracker:RemoveQuest(questId)
|
||||
questLog[questId] = nil
|
||||
end
|
||||
|
||||
QuestieCombatQueue:Queue(function()
|
||||
QuestieLib:CacheItemNames(questId)
|
||||
_QuestEventHandler:HandleQuestAccepted(questId)
|
||||
@@ -266,8 +287,10 @@ end
|
||||
---@param questId number
|
||||
---@return boolean true @if the function was successful, false otherwise
|
||||
function _QuestEventHandler:HandleQuestAccepted(questId)
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestEventHandler] HandleQuestAccepted - questId:", questId)
|
||||
local idx = QuestieCompat.GetQuestLogIndexByID(questId)
|
||||
if not idx then
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestEventHandler] HandleQuestAccepted - NO quest log index yet, retrying for questId:", questId)
|
||||
_QuestLogUpdateQueue:Insert(function()
|
||||
return _QuestEventHandler:HandleQuestAccepted(questId)
|
||||
end)
|
||||
@@ -277,7 +300,7 @@ function _QuestEventHandler:HandleQuestAccepted(questId)
|
||||
local cacheMiss, changes = QuestLogCache.CheckForChanges({ [questId] = true })
|
||||
if cacheMiss then
|
||||
-- if cacheMiss, no need to check changes as only 1 questId
|
||||
Questie:Debug(Questie.DEBUG_INFO, "Objectives are not cached yet")
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestEventHandler] HandleQuestAccepted - CACHE MISS for questId:", questId, "- retrying later")
|
||||
_QuestLogUpdateQueue:Insert(function()
|
||||
return _QuestEventHandler:HandleQuestAccepted(questId)
|
||||
end)
|
||||
@@ -285,7 +308,7 @@ function _QuestEventHandler:HandleQuestAccepted(questId)
|
||||
return false
|
||||
end
|
||||
|
||||
Questie:Debug(Questie.DEBUG_INFO, "Objectives are correct. Calling accept logic. quest:", questId)
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestEventHandler] HandleQuestAccepted - cache ready, calling AcceptQuest for questId:", questId)
|
||||
questLog[questId].state = QUEST_LOG_STATES.QUEST_ACCEPTED
|
||||
QuestieQuest:SetObjectivesDirty(questId)
|
||||
|
||||
@@ -447,6 +470,16 @@ function _QuestEventHandler:MarkQuestAsAbandoned(questId)
|
||||
"objectives were complete - treating as completed (auto-complete quest)")
|
||||
questEntry.state = QUEST_LOG_STATES.QUEST_TURNED_IN
|
||||
|
||||
-- Clear stale objective data so re-accepting this quest later doesn't
|
||||
-- inherit Cached "Completed = true" / "isUpdated = true" flags that
|
||||
-- would cause PopulateObjectiveNotes to skip drawing map pins.
|
||||
if quest then
|
||||
quest.Objectives = {}
|
||||
quest.WasComplete = nil
|
||||
quest.isComplete = nil
|
||||
end
|
||||
QuestieQuest:SetObjectivesDirty(questId)
|
||||
|
||||
QuestLogCache.RemoveQuest(questId)
|
||||
QuestieQuest:CompleteQuest(questId)
|
||||
QuestieJourney:CompleteQuest(questId)
|
||||
|
||||
+221
-19
@@ -472,28 +472,63 @@ function QuestieQuest:AcceptQuest(questId)
|
||||
local quest = QuestieDB.GetQuest(questId)
|
||||
|
||||
if quest then
|
||||
local complete = QuestieDB.IsComplete(questId)
|
||||
-- If any of these flags exsist then this quest has already once been accepted and is probobly in a failed state
|
||||
if (quest.WasComplete or quest.isComplete or complete == 0 or complete == -1) and (QuestiePlayer.currentQuestlog[questId]) then
|
||||
Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest] Accepted Quest:", questId,
|
||||
" Warning: This quest was once accepted and needs to be reset.")
|
||||
-- If any of these flags exist, this quest was previously accepted and may
|
||||
-- have stale completion state (e.g. complete-then-abandon-then-reaccept leaves
|
||||
-- quest.isComplete=true, WasComplete=true). Only check quest-object flags
|
||||
-- (WasComplete, isComplete), NOT QuestieDB.IsComplete, because IsComplete
|
||||
-- returns 0 for any incomplete quest — including brand new acceptances.
|
||||
-- The original code also checked complete==0 and complete==-1, but those
|
||||
-- combined with removing the currentQuestlog guard caused every quest
|
||||
-- acceptance to enter this block and unload freshly-drawn pins.
|
||||
-- DEBUG: Log stale flag state on accept
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] AcceptQuest state check - questId:", questId,
|
||||
" WasComplete:", tostring(quest.WasComplete),
|
||||
" isComplete:", tostring(quest.isComplete),
|
||||
" currentQuestlog:", tostring(QuestiePlayer.currentQuestlog[questId] and "present" or "nil"))
|
||||
|
||||
-- Reset quest log
|
||||
if quest.WasComplete or quest.isComplete then
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] AcceptQuest RESET BLOCK firing for questId:", questId)
|
||||
|
||||
-- Reset quest log (may already be nil after QUEST_REMOVED)
|
||||
QuestiePlayer.currentQuestlog[questId] = nil
|
||||
|
||||
-- Reset quest objectives
|
||||
quest.Objectives = {}
|
||||
-- Reset quest objectives (clear stale Completed/isUpdated flags that
|
||||
-- would prevent map pins from being drawn on re-accept)
|
||||
if type(quest.Objectives) == "table" then
|
||||
for k in pairs(quest.Objectives) do
|
||||
quest.Objectives[k] = nil
|
||||
end
|
||||
else
|
||||
quest.Objectives = {}
|
||||
end
|
||||
|
||||
-- Reset SpecialObjectives too
|
||||
if type(quest.SpecialObjectives) == "table" then
|
||||
for k in pairs(quest.SpecialObjectives) do
|
||||
quest.SpecialObjectives[k] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Reset quest flags
|
||||
quest.WasComplete = nil
|
||||
quest.isComplete = nil
|
||||
|
||||
-- Ensure isUpdated flags are reset so ObjectiveUpdate will refresh
|
||||
-- objective state from the quest log instead of short-circuiting
|
||||
QuestieQuest:SetObjectivesDirty(questId)
|
||||
|
||||
-- Reset tooltips
|
||||
QuestieTooltips:RemoveQuest(questId)
|
||||
|
||||
-- Unload map pins from previous acceptance so they don't linger
|
||||
QuestieMap:UnloadQuestFrames(questId)
|
||||
end
|
||||
|
||||
if not QuestiePlayer.currentQuestlog[questId] then
|
||||
Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest] Accepted Quest:", questId)
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] AcceptQuest NORMAL path - questId:", questId,
|
||||
" quest.isComplete:", tostring(quest.isComplete),
|
||||
" quest.WasComplete:", tostring(quest.WasComplete),
|
||||
" quest.Objectives:", quest.Objectives and next(quest.Objectives) and "has entries" or "empty")
|
||||
|
||||
QuestiePlayer.currentQuestlog[questId] = quest
|
||||
|
||||
@@ -537,8 +572,8 @@ function QuestieQuest:AcceptQuest(questId)
|
||||
end
|
||||
)
|
||||
else
|
||||
Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest] Accepted Quest:", questId,
|
||||
" Warning: Quest already exists, not adding")
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] AcceptQuest DUPLICATE - questId:", questId,
|
||||
" Warning: Quest already exists in currentQuestlog, not processing!")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -575,6 +610,14 @@ function QuestieQuest:CompleteQuest(questId)
|
||||
Questie.db.char.complete[13701] = true -- Horde Champion Marker
|
||||
Questie.db.char.complete[13687] = nil -- Horde Tournament Eligibility Marker
|
||||
end
|
||||
-- Clear stale objective data so a subsequent re-accept doesn't inherit
|
||||
-- Cached Completed=true / isUpdated=true flags that would cause
|
||||
-- PopulateObjectiveNotes to skip drawing map pins (bug: complete-abandon-reaccept).
|
||||
local quest = QuestieDB.GetQuest(questId)
|
||||
if quest and type(quest.Objectives) == "table" then
|
||||
quest.Objectives = {}
|
||||
end
|
||||
|
||||
QuestieMap:UnloadQuestFrames(questId)
|
||||
|
||||
-- Clear the pending-complete guard now that frames are unloaded
|
||||
@@ -620,6 +663,11 @@ function QuestieQuest:AbandonedQuest(questId)
|
||||
|
||||
local quest = QuestieDB.GetQuest(questId)
|
||||
if quest then
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] AbandonedQuest - questId:", questId,
|
||||
" WasComplete:", tostring(quest.WasComplete),
|
||||
" isComplete:", tostring(quest.isComplete),
|
||||
" Objectives:", quest.Objectives and next(quest.Objectives) and "has entries" or "empty")
|
||||
|
||||
-- Reset quest objectives
|
||||
quest.Objectives = {}
|
||||
|
||||
@@ -686,9 +734,31 @@ function QuestieQuest:UpdateQuest(questId)
|
||||
end
|
||||
QuestieQuest:PopulateQuestLogInfo(quest)
|
||||
|
||||
if QuestieQuest:ShouldShowQuestNotes(questId) then
|
||||
-- Defensive: clear stale isComplete/WasComplete after PopulateQuestLogInfo
|
||||
-- syncs quest log state. This catches cases where PopulateQuestLogInfo ran
|
||||
-- but the quest log cache didn't have the isComplete field (nil), in which
|
||||
-- case the else branch above wouldn't clear the flags.
|
||||
if quest.isComplete then
|
||||
local dbComplete = QuestieDB.IsComplete(questId)
|
||||
if dbComplete ~= 1 then
|
||||
quest.isComplete = nil
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:UpdateQuest] Cleared stale isComplete for quest:", questId)
|
||||
end
|
||||
end
|
||||
if quest.WasComplete then
|
||||
local dbComplete = QuestieDB.IsComplete(questId)
|
||||
if dbComplete ~= 1 then
|
||||
quest.WasComplete = nil
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:UpdateQuest] Cleared stale WasComplete for quest:", questId)
|
||||
end
|
||||
end
|
||||
|
||||
local showNotes = QuestieQuest:ShouldShowQuestNotes(questId)
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:UpdateQuest] ShouldShowQuestNotes:", tostring(showNotes), "questId:", questId)
|
||||
if showNotes then
|
||||
QuestieQuest:UpdateObjectiveNotes(quest)
|
||||
else
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:UpdateQuest] ShouldShowQuestNotes=false, removing tooltips for questId:", questId)
|
||||
QuestieTooltips:RemoveQuest(questId)
|
||||
end
|
||||
|
||||
@@ -760,6 +830,34 @@ function QuestieQuest:UpdateQuest(questId)
|
||||
QuestieQuest:PopulateObjectiveNotes(quest)
|
||||
AvailableQuests.CalculateAndDrawAll()
|
||||
else
|
||||
-- Robustness: ensure objective pins are present for incomplete quests.
|
||||
-- On Ascension, quest log cache can be stale after reload/abandon-reaccept,
|
||||
-- leaving objectives empty or AlreadySpawned pointing to dead frames.
|
||||
if showNotes then
|
||||
local hasObjectives = quest.Objectives and table.getn(quest.Objectives) > 0
|
||||
local hasFrames = QuestieMap.questIdFrames[questId] ~= nil
|
||||
|
||||
if not hasObjectives then
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP,
|
||||
"[QuestieQuest:UpdateQuest] Objectives missing for incomplete quest, re-populating:", questId)
|
||||
QuestieQuest:PopulateQuestLogInfo(quest)
|
||||
hasObjectives = quest.Objectives and table.getn(quest.Objectives) > 0
|
||||
if hasObjectives then
|
||||
QuestieQuest:PopulateObjectiveNotes(quest)
|
||||
end
|
||||
elseif not hasFrames then
|
||||
-- Objectives exist but no frames on map: AlreadySpawned may be stale
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP,
|
||||
"[QuestieQuest:UpdateQuest] Incomplete quest has objectives but no map frames, clearing AlreadySpawned:", questId)
|
||||
for _, objective in pairs(quest.Objectives) do
|
||||
if objective.AlreadySpawned then
|
||||
objective.AlreadySpawned = {}
|
||||
end
|
||||
end
|
||||
QuestieQuest:PopulateObjectiveNotes(quest)
|
||||
end
|
||||
end
|
||||
|
||||
-- Sometimes objective(s) are all complete but the quest doesn't get flagged as "1". So far the only
|
||||
-- quests I've found that does this are quests involving an item(s). Checks all objective(s) and if they
|
||||
-- are all complete, simulate a "Complete Quest" so the quest finisher appears on the map.
|
||||
@@ -1048,7 +1146,12 @@ end
|
||||
-- iterate all notes, update / remove as needed
|
||||
---@param quest Quest
|
||||
function QuestieQuest:UpdateObjectiveNotes(quest)
|
||||
-- DEBUG: Log currentQuestlog state
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] UpdateObjectiveNotes - questId:", quest.Id,
|
||||
" currentQuestlog present:", tostring(QuestiePlayer.currentQuestlog and QuestiePlayer.currentQuestlog[quest.Id] and "yes" or "no"))
|
||||
|
||||
if (not QuestiePlayer.currentQuestlog) or (not QuestiePlayer.currentQuestlog[quest.Id]) then
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] UpdateObjectiveNotes - EARLY RETURN: quest not in currentQuestlog, questId:", quest.Id)
|
||||
return
|
||||
end
|
||||
|
||||
@@ -1342,14 +1445,17 @@ end
|
||||
---@param objective QuestObjective
|
||||
---@param blockItemTooltips any
|
||||
function QuestieQuest:PopulateObjective(quest, objectiveIndex, objective, blockItemTooltips) -- must be p-called
|
||||
Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:PopulateObjective]", objective.Description)
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:PopulateObjective] questId:", quest.Id,
|
||||
" objIdx:", objectiveIndex, " Desc:", objective.Description,
|
||||
" Completed:", tostring(objective.Completed),
|
||||
" quest.isComplete:", tostring(quest.isComplete))
|
||||
|
||||
if (not objective.Update) then
|
||||
-- No Update function means static/pre-populated objective.
|
||||
-- Still check completion state so icons are removed if this objective was
|
||||
-- already marked complete from a previous update cycle.
|
||||
if objective.Completed or quest.isComplete then
|
||||
Questie:Debug(Questie.DEBUG_INFO,
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP,
|
||||
"[QuestieQuest:PopulateObjective] - No Update fn but objective is complete, unloading icons.")
|
||||
_UnloadAlreadySpawnedIcons(objective)
|
||||
end
|
||||
@@ -1358,7 +1464,24 @@ function QuestieQuest:PopulateObjective(quest, objectiveIndex, objective, blockI
|
||||
|
||||
objective:Update()
|
||||
|
||||
-- Defensive: clear stale quest.isComplete if quest DB says the quest is NOT
|
||||
-- complete. The DB cache persists quest objects across acceptance cycles
|
||||
-- (complete → abandon → reaccept), and PopulateQuestLogInfo may not have
|
||||
-- run yet for this specific path (e.g., Learner invalidation).
|
||||
if quest.isComplete then
|
||||
local dbComplete = QuestieDB.IsComplete(quest.Id)
|
||||
if dbComplete ~= 1 then
|
||||
quest.isComplete = nil
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:PopulateObjective] Cleared stale isComplete for quest:", quest.Id)
|
||||
end
|
||||
end
|
||||
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:PopulateObjective] POST-Update questId:", quest.Id,
|
||||
" objIdx:", objectiveIndex, " Completed:", tostring(objective.Completed),
|
||||
" isComplete:", tostring(quest.isComplete), " HasUpdate:", tostring(objective.Update ~= nil))
|
||||
|
||||
if QuestieQuest.ShouldHideObjective(objective) then
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:PopulateObjective] HIDDEN by ShouldHideObjective, unloading icons for objIdx:", objectiveIndex)
|
||||
_UnloadAlreadySpawnedIcons(objective)
|
||||
return
|
||||
end
|
||||
@@ -1377,6 +1500,8 @@ function QuestieQuest:PopulateObjective(quest, objectiveIndex, objective, blockI
|
||||
_RegisterObjectiveTooltips(objective, quest.Id, blockItemTooltips)
|
||||
|
||||
if completed or quest.isComplete then
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:PopulateObjective] SKIPPING objective (completed):",
|
||||
objective.Description, "completed:", tostring(completed), "quest.isComplete:", tostring(quest.isComplete))
|
||||
_UnloadAlreadySpawnedIcons(objective)
|
||||
return
|
||||
end
|
||||
@@ -1386,6 +1511,8 @@ function QuestieQuest:PopulateObjective(quest, objectiveIndex, objective, blockI
|
||||
end
|
||||
|
||||
if objective.spawnList and next(objective.spawnList) then
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:PopulateObjective] spawnList present for quest:", quest.Id,
|
||||
"objIdx:", objectiveIndex, "spawnList entries:", (function() local n=0 for _ in pairs(objective.spawnList) do n=n+1 end return n end)())
|
||||
local maxPerType = 300
|
||||
|
||||
if Questie.db.profile.enableIconLimit and Questie.db.profile.iconLimit < maxPerType then
|
||||
@@ -1448,6 +1575,9 @@ function QuestieQuest:PopulateObjective(quest, objectiveIndex, objective, blockI
|
||||
local iconsToDraw, _ = _DetermineIconsToDraw(quest, objective, objectiveIndex, objectiveCenter)
|
||||
local icon, iconPerZone = _DrawObjectiveIcons(quest.Id, iconsToDraw, objective, maxPerType)
|
||||
_DrawObjectiveWaypoints(objective, icon, iconPerZone)
|
||||
else
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:PopulateObjective] NO spawnList for quest:", quest.Id,
|
||||
"objIdx:", objectiveIndex, "spawnList:", tostring(objective.spawnList))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1519,7 +1649,10 @@ end
|
||||
---@param objectiveIndex ObjectiveIndex
|
||||
---@param objectiveCenter {x:X, y:Y}
|
||||
_DetermineIconsToDraw = function(quest, objective, objectiveIndex, objectiveCenter)
|
||||
Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:_DetermineIconsToDraw]")
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:_DetermineIconsToDraw] quest:", quest.Id,
|
||||
"objective:", objective.Description, "spawnList:", objective.spawnList and next(objective.spawnList) and "has entries" or "nil/empty",
|
||||
"AlreadySpawned:", objective.AlreadySpawned and next(objective.AlreadySpawned) and "has entries" or "empty",
|
||||
"Completed:", tostring(objective.Completed), "enableObjectives:", tostring(Questie.db.profile.enableObjectives))
|
||||
|
||||
local iconsToDraw = {}
|
||||
local spawnItemId
|
||||
@@ -1535,6 +1668,7 @@ _DetermineIconsToDraw = function(quest, objective, objectiveIndex, objectiveCent
|
||||
end
|
||||
|
||||
if (not objective.AlreadySpawned[id]) and (not objective.Completed) and Questie.db.profile.enableObjectives then
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:_DetermineIconsToDraw] CREATING icon entry for id:", id, "quest:", quest.Id)
|
||||
local data = {
|
||||
Id = quest.Id,
|
||||
ObjectiveIndex = objectiveIndex,
|
||||
@@ -1605,7 +1739,15 @@ _DetermineIconsToDraw = function(quest, objective, objectiveIndex, objectiveCent
|
||||
end
|
||||
|
||||
_DrawObjectiveIcons = function(questId, iconsToDraw, objective, maxPerType)
|
||||
Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:_DrawObjectiveIcons] Adding Icons for quest:", questId)
|
||||
local iconCount = 0
|
||||
local _, _ = next(iconsToDraw)
|
||||
if _ then
|
||||
local n = 0
|
||||
for _ in pairs(iconsToDraw) do n = n + 1 end
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:_DrawObjectiveIcons] Drawing", n, "icon groups for quest:", questId)
|
||||
else
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:_DrawObjectiveIcons] NO icons to draw for quest:", questId)
|
||||
end
|
||||
|
||||
local spawnedIconCount = 0
|
||||
local icon
|
||||
@@ -1747,7 +1889,23 @@ function QuestieQuest:PopulateObjectiveNotes(quest) -- this should be renamed to
|
||||
return
|
||||
end
|
||||
|
||||
if QuestieDB.IsComplete(quest.Id) == 1 then
|
||||
local dbComplete = QuestieDB.IsComplete(quest.Id)
|
||||
|
||||
-- DEBUG: Log PopulateObjectiveNotes state
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] PopulateObjectiveNotes - questId:", quest.Id,
|
||||
" quest.isComplete:", tostring(quest.isComplete),
|
||||
" dbComplete:", tostring(dbComplete),
|
||||
" Objectives count:", quest.Objectives and next(quest.Objectives) and "has entries" or "empty")
|
||||
|
||||
-- Defensive: clear stale quest.isComplete flag if the quest log doesn't confirm
|
||||
-- completion. Without this, a re-accepted quest that was previously completed can
|
||||
-- have isComplete=true while the quest log shows it as incomplete.
|
||||
if quest.isComplete and dbComplete ~= 1 then
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] PopulateObjectiveNotes - clearing stale isComplete for questId:", quest.Id)
|
||||
quest.isComplete = nil
|
||||
end
|
||||
|
||||
if dbComplete == 1 then
|
||||
Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:PopulateObjectiveNotes] Quest Complete! Adding Finisher for:",
|
||||
quest.Id)
|
||||
|
||||
@@ -1778,14 +1936,33 @@ function QuestieQuest:PopulateQuestLogInfo(quest)
|
||||
return nil
|
||||
end
|
||||
|
||||
Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:PopulateQuestLogInfo] ", quest.Id)
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] PopulateQuestLogInfo - questId:", quest.Id,
|
||||
" Objectives:", quest.Objectives and next(quest.Objectives) and "has entries" or "empty",
|
||||
" isComplete:", tostring(quest.isComplete),
|
||||
" WasComplete:", tostring(quest.WasComplete))
|
||||
|
||||
local questLogEngtry = QuestLogCache.GetQuest(quest.Id) -- DO NOT MODIFY THE RETURNED TABLE
|
||||
|
||||
if (not questLogEngtry) then return end
|
||||
if (not questLogEngtry) then
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] PopulateQuestLogInfo - CACHE MISS for questId:", quest.Id, "- returning early!")
|
||||
return
|
||||
end
|
||||
|
||||
-- Sync quest.isComplete with quest log state. The cache in QuestieDB persists
|
||||
-- quest objects across acceptance cycles (complete → abandon → reaccept), so
|
||||
-- stale isComplete/WasComplete flags from a previous acceptance must be cleared
|
||||
-- when the quest log says the quest is no longer complete.
|
||||
if questLogEngtry.isComplete ~= nil and questLogEngtry.isComplete == 1 then
|
||||
quest.isComplete = true
|
||||
else
|
||||
quest.isComplete = nil
|
||||
-- Also clear WasComplete when quest log confirms the quest is NOT complete.
|
||||
-- Without this, the isComplete==0 branch in UpdateQuest sees stale
|
||||
-- WasComplete=true and triggers an unnecessary reset sequence.
|
||||
if quest.WasComplete then
|
||||
quest.WasComplete = nil
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:PopulateQuestLogInfo] Cleared stale WasComplete for quest:", quest.Id)
|
||||
end
|
||||
end
|
||||
|
||||
-- Live fallback quests (no static DB entry) manage their own Objectives.
|
||||
@@ -1980,6 +2157,31 @@ function _QuestieQuest.ObjectiveUpdate(self)
|
||||
end
|
||||
end
|
||||
|
||||
--- Lazily build an objective's spawnList from the spawn call table (includes
|
||||
--- QuestieLearner-injected data). Used by QuestieArrow to resolve targets when
|
||||
--- the spawnList hasn't been populated yet (e.g. after a quest re-accept cycle).
|
||||
---@param objective table @An objective entry from quest.Objectives or quest.SpecialObjectives
|
||||
---@param objectiveData table? @Corresponding ObjectiveData entry (falls back to the objective itself for SpecialObjectives)
|
||||
---@return table|nil @The built spawnList, or nil if no handler matched
|
||||
function QuestieQuest:BuildObjectiveSpawnList(objective, objectiveData)
|
||||
if not objective then return nil end
|
||||
-- Already populated — nothing to do
|
||||
if objective.spawnList and next(objective.spawnList) then
|
||||
return objective.spawnList
|
||||
end
|
||||
-- Try the explicit ObjectiveData first (normal objectives)
|
||||
if objectiveData and objectiveData.Type and _QuestieQuest.objectiveSpawnListCallTable[objectiveData.Type] then
|
||||
objective.spawnList = _QuestieQuest.objectiveSpawnListCallTable[objectiveData.Type](objective.Id, objective, objectiveData)
|
||||
return objective.spawnList
|
||||
end
|
||||
-- SpecialObjectives and fallback: use the objective's own Type
|
||||
if objective.Type and _QuestieQuest.objectiveSpawnListCallTable[objective.Type] then
|
||||
objective.spawnList = _QuestieQuest.objectiveSpawnListCallTable[objective.Type](objective.Id, objective, objective)
|
||||
return objective.spawnList
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---@param questId number
|
||||
---@return table<ObjectiveIndex, QuestLogCacheObjectiveData>|nil @DO NOT EDIT RETURNED TABLE
|
||||
function QuestieQuest:GetAllLeaderBoardDetails(questId)
|
||||
|
||||
@@ -120,7 +120,23 @@ monster = function(npcId, objective)
|
||||
end
|
||||
|
||||
local name = QuestieDB.QueryNPCSingle(npcId, "name")
|
||||
if (not name) then
|
||||
if not name or name == "" then
|
||||
-- Last resort: extract NPC name from objective description text.
|
||||
-- This mirrors the name-parsing logic in the killcredit function.
|
||||
if objective then
|
||||
local desc = objective.Description or objective.text
|
||||
if desc then
|
||||
name = desc:match("^%d+/%d+%s+(.+)$") or desc:match("^(.-):%s*%d+/%d+$")
|
||||
if not name then
|
||||
name = desc:gsub("%d+/%d+", ""):gsub("[:!?,.%(%)%[%]]", ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
end
|
||||
if name and name ~= "" then
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[monster] Using objective description as name fallback for NPC:", npcId, name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if not name or name == "" then
|
||||
Questie:Debug(Questie.DEBUG_CRITICAL, "Name missing for NPC:", npcId)
|
||||
return nil
|
||||
end
|
||||
@@ -131,6 +147,25 @@ monster = function(npcId, objective)
|
||||
spawns = {}
|
||||
end
|
||||
|
||||
-- Learner safety net: when prioritizeMyData is enabled and the Learner has
|
||||
-- verified spawn data for this NPC, prefer it over compiled DB spawns.
|
||||
-- This catches edge cases where the npcDataOverrides chain doesn't fully
|
||||
-- replace retail positions (e.g. format migration gaps, timing issues).
|
||||
if Questie.IsAscension and Questie.dbLearner and Questie.dbLearner.global then
|
||||
local ld = Questie.dbLearner.global
|
||||
if ld.settings and ld.settings.prioritizeMyData then
|
||||
local learnedNpc = ld.npcs and ld.npcs[npcId]
|
||||
if learnedNpc then
|
||||
local learnedSpawns = learnedNpc[7]
|
||||
local threshold = ld.settings.minConfidencePins or 1
|
||||
if learnedSpawns and next(learnedSpawns) and learnedNpc.mc and learnedNpc.mc >= threshold then
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[monster] Preferring learned spawns for NPC:", npcId, "(mc=" .. tostring(learnedNpc.mc) .. ")")
|
||||
spawns = learnedSpawns
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local rank = QuestieDB.QueryNPCSingle(npcId, "rank")
|
||||
|
||||
local enableSpawns = not QuestieCorrections.questNPCBlacklist[npcId]
|
||||
|
||||
+349
-57
@@ -95,11 +95,28 @@ QuestieLearner.data = nil
|
||||
------------------------------------------------------------------------
|
||||
|
||||
local function GetZoneId()
|
||||
-- On WotLK/Ascension, C_Map.GetBestMapForUnit returns a uiMapId (e.g. 1241 for Sunstrider).
|
||||
-- Questie NPC spawns are keyed by areaId (e.g. 3430), so we must convert.
|
||||
-- Prefer the most specific zone available: GetRealZoneText() returns the
|
||||
-- sub-zone name when the player is on a child map (e.g. "Sunstrider Isle"
|
||||
-- on map 1241 → areaId 3431), and the parent zone name otherwise (e.g.
|
||||
-- "Eversong Woods" → areaId 3430). Using the sub-zone is correct because
|
||||
-- Questie resolves subzones to parents via GetParentZoneId() automatically.
|
||||
local zoneText = GetRealZoneText and GetRealZoneText() or ""
|
||||
if zoneText ~= "" then
|
||||
local areaId = _Learner.zoneCache[zoneText]
|
||||
if not areaId and l10n and l10n.GetAreaIdByLocalName then
|
||||
areaId = l10n:GetAreaIdByLocalName(zoneText)
|
||||
if areaId and areaId > 0 then
|
||||
_Learner.zoneCache[zoneText] = areaId
|
||||
end
|
||||
end
|
||||
if areaId and areaId > 0 then
|
||||
return areaId
|
||||
end
|
||||
end
|
||||
|
||||
-- Fallback: uiMapId-based conversion (e.g. 1241→3431 for Sunstrider).
|
||||
local uiMapId = C_Map and C_Map.GetBestMapForUnit and C_Map.GetBestMapForUnit("player")
|
||||
if uiMapId then
|
||||
-- Try ZoneDB reverse lookup first (covers Ascension overrides like 1241→3430)
|
||||
if ZoneDB and ZoneDB.GetAreaIdByUiMapId then
|
||||
local areaId = ZoneDB:GetAreaIdByUiMapId(uiMapId)
|
||||
if areaId and areaId > 0 then
|
||||
@@ -312,57 +329,93 @@ end
|
||||
-- reference the given npcId. Unloads existing world/minimap icons, resets
|
||||
-- tooltip registration, and forces the map system to rebuild spawn lists from
|
||||
-- QuestieDB on the next update — picking up newly learned coordinates in real time.
|
||||
-- Helper: check if a single objective references the given npcId and, if so,
|
||||
-- unload its icons and reset its cached spawnList so the map rebuilds it.
|
||||
-- Returns true if the objective was invalidated.
|
||||
local function _TryInvalidateObjective(objective, npcId, quest)
|
||||
local shouldInvalidate = false
|
||||
-- Monster objectives reference NPCs directly in spawnList keys
|
||||
if objective.spawnList then
|
||||
if objective.spawnList[npcId] then
|
||||
shouldInvalidate = true
|
||||
end
|
||||
-- Also check killcredit IdList
|
||||
if not shouldInvalidate and objective.IdList then
|
||||
for _, id in ipairs(objective.IdList) do
|
||||
if id == npcId then shouldInvalidate = true; break end
|
||||
end
|
||||
end
|
||||
else
|
||||
-- spawnList is nil (first-ever encounter, never populated).
|
||||
-- Check the quest's ObjectiveData for NPC references so killcredit
|
||||
-- and item objectives are still invalidated on the first kill.
|
||||
if quest and quest.ObjectiveData and objective.Index then
|
||||
local objData = quest.ObjectiveData[objective.Index]
|
||||
if objData and objData.IdList then
|
||||
for _, id in ipairs(objData.IdList) do
|
||||
if id == npcId then shouldInvalidate = true; break end
|
||||
end
|
||||
end
|
||||
-- Also match the primary objective Id (e.g. single-target monster objectives)
|
||||
if not shouldInvalidate and objData and objData.Id == npcId then
|
||||
shouldInvalidate = true
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Fallback: if objective Id matches the NPC (some objectives use NPC as their primary Id)
|
||||
if not shouldInvalidate and objective.Id == npcId then
|
||||
shouldInvalidate = true
|
||||
end
|
||||
if shouldInvalidate then
|
||||
-- Unload existing icons manually so frames are removed from map/minimap.
|
||||
-- We can't call QuestieQuest's local _UnloadAlreadySpawnedIcons from here,
|
||||
-- so we iterate the refs directly.
|
||||
if objective.AlreadySpawned then
|
||||
for _, spawn in pairs(objective.AlreadySpawned) do
|
||||
if spawn then
|
||||
if spawn.mapRefs then
|
||||
for _, mapIcon in ipairs(spawn.mapRefs) do
|
||||
if mapIcon and mapIcon.Unload then mapIcon:Unload() end
|
||||
end
|
||||
end
|
||||
if spawn.minimapRefs then
|
||||
for _, minimapIcon in ipairs(spawn.minimapRefs) do
|
||||
if minimapIcon and minimapIcon.Unload then minimapIcon:Unload() end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
objective.spawnList = nil
|
||||
objective.AlreadySpawned = {} -- empty table, NOT nil (_DetermineIconsToDraw indexes this)
|
||||
objective.hasRegisteredTooltips = false
|
||||
objective.registeredItemTooltips = false
|
||||
end
|
||||
return shouldInvalidate
|
||||
end
|
||||
|
||||
local function _InvalidateSpawnListsForNPC(npcId)
|
||||
if not QuestieQuest or not QuestiePlayer or not QuestiePlayer.currentQuestlog then return end
|
||||
local timer = (C_Timer) or (QuestieCompat and QuestieCompat.C_Timer)
|
||||
local questsToRefresh = {}
|
||||
for questId, _ in pairs(QuestiePlayer.currentQuestlog) do
|
||||
local quest = QuestieDB.GetQuest and QuestieDB.GetQuest(questId)
|
||||
if quest and quest.Objectives then
|
||||
if quest then
|
||||
local needsUnload = false
|
||||
for _, objective in pairs(quest.Objectives) do
|
||||
local shouldInvalidate = false
|
||||
-- Monster objectives reference NPCs directly in spawnList keys
|
||||
if objective.spawnList then
|
||||
if objective.spawnList[npcId] then
|
||||
shouldInvalidate = true
|
||||
end
|
||||
-- Also check killcredit IdList
|
||||
if not shouldInvalidate and objective.IdList then
|
||||
for _, id in ipairs(objective.IdList) do
|
||||
if id == npcId then shouldInvalidate = true; break end
|
||||
end
|
||||
-- Scan standard Objectives
|
||||
if quest.Objectives then
|
||||
for _, objective in pairs(quest.Objectives) do
|
||||
if _TryInvalidateObjective(objective, npcId, quest) then
|
||||
needsUnload = true
|
||||
end
|
||||
end
|
||||
-- Fallback: if objective Id matches the NPC (some objectives use NPC as their primary Id)
|
||||
if not shouldInvalidate and objective.Id == npcId then
|
||||
shouldInvalidate = true
|
||||
end
|
||||
if shouldInvalidate then
|
||||
-- Unload existing icons manually so frames are removed from map/minimap.
|
||||
-- We can't call QuestieQuest's local _UnloadAlreadySpawnedIcons from here,
|
||||
-- so we iterate the refs directly.
|
||||
if objective.AlreadySpawned then
|
||||
for _, spawn in pairs(objective.AlreadySpawned) do
|
||||
if spawn then
|
||||
if spawn.mapRefs then
|
||||
for _, mapIcon in ipairs(spawn.mapRefs) do
|
||||
if mapIcon and mapIcon.Unload then mapIcon:Unload() end
|
||||
end
|
||||
end
|
||||
if spawn.minimapRefs then
|
||||
for _, minimapIcon in ipairs(spawn.minimapRefs) do
|
||||
if minimapIcon and minimapIcon.Unload then minimapIcon:Unload() end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Scan SpecialObjectives (demonic runestones, custom Ascension objectives, etc.)
|
||||
if quest.SpecialObjectives then
|
||||
for _, objective in pairs(quest.SpecialObjectives) do
|
||||
if _TryInvalidateObjective(objective, npcId, quest) then
|
||||
needsUnload = true
|
||||
end
|
||||
objective.spawnList = nil
|
||||
objective.AlreadySpawned = {} -- empty table, NOT nil (_DetermineIconsToDraw indexes this)
|
||||
objective.hasRegisteredTooltips = false
|
||||
objective.registeredItemTooltips = false
|
||||
needsUnload = true
|
||||
end
|
||||
end
|
||||
if needsUnload then
|
||||
@@ -648,10 +701,21 @@ end
|
||||
-- NPC learning
|
||||
------------------------------------------------------------------------
|
||||
|
||||
-- Player-spawned NPCs that should never be learned (totems, guardians, etc.)
|
||||
local PLAYER_SPAWNED_NPC_SET = {
|
||||
[2523] = true, -- Searing Totem
|
||||
[2630] = true, -- Earthbind Totem
|
||||
[10183] = true, -- Moonflare Totem
|
||||
[1103907] = true, -- Healing Stream Totem III
|
||||
[1107398] = true, -- Stoneclaw Totem V
|
||||
}
|
||||
|
||||
function QuestieLearner:LearnNPC(npcId, name, level, subName, npcFlags, factionString, spawnX, spawnY, spawnZoneId)
|
||||
if not self:IsEnabled() then return end
|
||||
if not Questie.dbLearner.global.settings.learnNpcs then return end
|
||||
if not npcId or npcId <= 0 then return end
|
||||
-- Never learn player-spawned totems
|
||||
if PLAYER_SPAWNED_NPC_SET[npcId] then return end
|
||||
|
||||
-- Use provided spawn coords (e.g. from kill event) or fall back to current player position
|
||||
local zoneId = spawnZoneId or GetZoneId()
|
||||
@@ -669,7 +733,7 @@ function QuestieLearner:LearnNPC(npcId, name, level, subName, npcFlags, factionS
|
||||
Questie.dbLearner.global.npcs[npcId] = existing
|
||||
end
|
||||
|
||||
if name and not existing[1] then existing[1] = name end
|
||||
if name and name ~= "" and (not existing[1] or existing[1] == "") then existing[1] = name end
|
||||
if level then
|
||||
if not existing[4] or level < existing[4] then existing[4] = level end
|
||||
if not existing[5] or level > existing[5] then existing[5] = level end
|
||||
@@ -694,9 +758,9 @@ function QuestieLearner:LearnNPC(npcId, name, level, subName, npcFlags, factionS
|
||||
if not ovr then
|
||||
QuestieDB.npcDataOverrides[npcId] = existing
|
||||
else
|
||||
-- Merge: fill missing fields only
|
||||
-- Merge: fill missing fields; also overwrite empty-string names
|
||||
for k, v in pairs(existing) do
|
||||
if ovr[k] == nil then ovr[k] = v end
|
||||
if ovr[k] == nil or (k == 1 and ovr[k] == "") then ovr[k] = v end
|
||||
end
|
||||
-- Always merge spawn coords
|
||||
if existing[7] then
|
||||
@@ -718,12 +782,11 @@ function QuestieLearner:LearnNPC(npcId, name, level, subName, npcFlags, factionS
|
||||
if isNew then
|
||||
Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] New NPC learned:", npcId, name or "?")
|
||||
CrossLinkAfterNPC(npcId)
|
||||
else
|
||||
-- Existing NPC got new spawn data: invalidate cached spawnLists
|
||||
-- for any active quest objective that references this NPC so the
|
||||
-- map system rebuilds them with fresh data on next update.
|
||||
_InvalidateSpawnListsForNPC(npcId)
|
||||
end
|
||||
-- Existing NPC got new spawn data: invalidate cached spawnLists
|
||||
-- for any active quest objective that references this NPC so the
|
||||
-- map system rebuilds them with fresh data on next update.
|
||||
_InvalidateSpawnListsForNPC(npcId)
|
||||
_Learner:BroadcastIfCommsAvailable("NPC", npcId, existing)
|
||||
end
|
||||
|
||||
@@ -1117,11 +1180,65 @@ function QuestieLearner:InjectLearnedData()
|
||||
if not EnsureLearnedData() then return end
|
||||
|
||||
local learned = Questie.dbLearner.global
|
||||
-- Migrate old-format NPC data ([4]=spawns, [5]=zoneId) to new format ([7]=spawns, [9]=zoneId)
|
||||
-- Always merge [4] into [7], even when [7] already has partial data from a recent session.
|
||||
for npcId, data in pairs(learned.npcs) do
|
||||
if type(data[4]) == "table" then
|
||||
if data[7] == nil then
|
||||
-- Simple move: no [7] exists yet
|
||||
data[7] = data[4]
|
||||
else
|
||||
-- Merge: [7] has partial data, consolidate [4] coordinates into it
|
||||
for zoneId, coords in pairs(data[4]) do
|
||||
data[7][zoneId] = data[7][zoneId] or {}
|
||||
for _, coord in ipairs(coords) do
|
||||
InsertIfNewBucket(data[7][zoneId], coord[1], coord[2])
|
||||
end
|
||||
end
|
||||
end
|
||||
data[4] = nil
|
||||
end
|
||||
if type(data[5]) == "number" and data[9] == nil then
|
||||
data[9] = data[5]
|
||||
data[5] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Merge character-specific learned NPC data into global pool
|
||||
if Questie.db and Questie.db.char and Questie.db.char.npcs then
|
||||
for npcId, data in pairs(Questie.db.char.npcs) do
|
||||
local globalData = learned.npcs[npcId]
|
||||
if not globalData then
|
||||
learned.npcs[npcId] = data
|
||||
else
|
||||
-- Merge spawns: char data may be old ([4]) or new ([7]) format
|
||||
local charSpawns = data[7] or data[4]
|
||||
local globalSpawns = globalData[7] or globalData[4]
|
||||
if charSpawns then
|
||||
if not globalSpawns then
|
||||
globalData[7] = {}
|
||||
globalSpawns = globalData[7]
|
||||
end
|
||||
for zoneId, coords in pairs(charSpawns) do
|
||||
globalSpawns[zoneId] = globalSpawns[zoneId] or {}
|
||||
for _, coord in ipairs(coords) do
|
||||
InsertIfNewBucket(globalSpawns[zoneId], coord[1], coord[2])
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Adopt zoneId if missing
|
||||
if not globalData[9] and data[9] then globalData[9] = data[9] end
|
||||
if not globalData[5] and data[5] then globalData[5] = data[5] end
|
||||
end
|
||||
end
|
||||
Questie.db.char.npcs = nil
|
||||
end
|
||||
|
||||
local npcCount, questCount, itemCount, objectCount = 0, 0, 0, 0
|
||||
|
||||
-- Migration: fix spawn zone keys that were stored as uiMapId instead of areaId.
|
||||
-- Before the GetZoneId() fix, kills on maps like Sunstrider Isle (uiMapId 1241)
|
||||
-- were stored under key 1241 instead of the correct areaId 3430.
|
||||
-- were stored under key 1241 instead of the correct areaId 3431.
|
||||
-- Convert any uiMapId keys to areaId using ZoneDB.
|
||||
local zonesFixed = 0
|
||||
for npcId, data in pairs(learned.npcs) do
|
||||
@@ -1181,6 +1298,82 @@ function QuestieLearner:InjectLearnedData()
|
||||
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Migrated", zonesFixed, "spawn zone keys from uiMapId to areaId")
|
||||
end
|
||||
|
||||
-- Also fix [9] zone field for NPCs and [5] zone field for Objects
|
||||
-- that were stored as uiMapId instead of areaId (e.g. 1241 → 3431).
|
||||
local fieldsFixed = 0
|
||||
for npcId, data in pairs(learned.npcs) do
|
||||
if type(data[9]) == "number" and ZoneDB and ZoneDB.GetAreaIdByUiMapId then
|
||||
local maybeAreaId = ZoneDB:GetAreaIdByUiMapId(data[9])
|
||||
if maybeAreaId and maybeAreaId ~= data[9] then
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] NPC", npcId, "zone field [9]", data[9], "->", maybeAreaId)
|
||||
data[9] = maybeAreaId
|
||||
fieldsFixed = fieldsFixed + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
for objId, data in pairs(learned.objects) do
|
||||
if type(data[5]) == "number" and ZoneDB and ZoneDB.GetAreaIdByUiMapId then
|
||||
local maybeAreaId = ZoneDB:GetAreaIdByUiMapId(data[5])
|
||||
if maybeAreaId and maybeAreaId ~= data[5] then
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] Object", objId, "zone field [5]", data[5], "->", maybeAreaId)
|
||||
data[5] = maybeAreaId
|
||||
fieldsFixed = fieldsFixed + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
if fieldsFixed > 0 then
|
||||
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Fixed", fieldsFixed, "zone fields from uiMapId to areaId")
|
||||
end
|
||||
|
||||
-- Purge player-spawned totems from learned NPCs (they are not real world spawns)
|
||||
-- Also purge NPCs with entirely empty spawn data (stale learner artifacts)
|
||||
local purgedNpcs = 0
|
||||
local PLAYER_SPAWNED_NPCS = {
|
||||
[2523] = true, -- Searing Totem
|
||||
[2630] = true, -- Earthbind Totem
|
||||
[10183] = true, -- Moonflare Totem
|
||||
[1103907] = true, -- Healing Stream Totem III
|
||||
[1107398] = true, -- Stoneclaw Totem V
|
||||
}
|
||||
for npcId, data in pairs(learned.npcs) do
|
||||
if PLAYER_SPAWNED_NPCS[npcId] then
|
||||
learned.npcs[npcId] = nil
|
||||
purgedNpcs = purgedNpcs + 1
|
||||
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Purged player-spawned NPC", npcId, data[1] or "?")
|
||||
elseif data[7] then
|
||||
-- Check for empty spawn table (no coords at all = stale learner artifact)
|
||||
local hasCoords = false
|
||||
for zoneKey, coords in pairs(data[7]) do
|
||||
if type(coords) == "table" and #coords > 0 then
|
||||
hasCoords = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not hasCoords then
|
||||
learned.npcs[npcId] = nil
|
||||
purgedNpcs = purgedNpcs + 1
|
||||
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Purged NPC with empty spawns", npcId, data[1] or "?")
|
||||
end
|
||||
end
|
||||
end
|
||||
if purgedNpcs > 0 then
|
||||
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Purged", purgedNpcs, "invalid NPCs from learned data")
|
||||
end
|
||||
|
||||
-- Purge Object entries that duplicate NPC entries (mobs learned as both NPC and Object).
|
||||
-- NPC data is richer (has names, quest IDs), so keep the NPC version and remove the Object.
|
||||
local dupObjectsRemoved = 0
|
||||
for objId, _ in pairs(learned.objects) do
|
||||
if learned.npcs[objId] then
|
||||
learned.objects[objId] = nil
|
||||
dupObjectsRemoved = dupObjectsRemoved + 1
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] Removed duplicate Object", objId, "(NPC version exists)")
|
||||
end
|
||||
end
|
||||
if dupObjectsRemoved > 0 then
|
||||
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Removed", dupObjectsRemoved, "Object entries that duplicate NPC entries")
|
||||
end
|
||||
|
||||
-- 1. NPCs
|
||||
local npcIdsToFix = {}
|
||||
for npcId, data in pairs(learned.npcs) do
|
||||
@@ -1216,6 +1409,87 @@ function QuestieLearner:InjectLearnedData()
|
||||
learned.npcs[old] = nil
|
||||
end
|
||||
|
||||
-- Diagnostic: log how many NPCs were injected with spawn overrides
|
||||
local spawnOverrideCount = 0
|
||||
for nid, ovr in pairs(QuestieDB.npcDataOverrides) do
|
||||
if ovr[7] and next(ovr[7]) then
|
||||
spawnOverrideCount = spawnOverrideCount + 1
|
||||
end
|
||||
end
|
||||
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieLearner] InjectLearnedData: injected", npcCount, "NPCs (", spawnOverrideCount, "with spawn overrides)")
|
||||
|
||||
-- Purge garbage quest entries: quests with no name [1] and only mc/ls metadata.
|
||||
-- These are Ascension internal tracking artifacts (hash-like IDs) with no real quest data.
|
||||
-- Also purge quest 788 ("Mottled Boar slain") which is an objective text, not a quest name.
|
||||
local purgedQuests = 0
|
||||
local OBJECTIVE_TEXT_QUEST_IDS = {
|
||||
[788] = true, -- "Mottled Boar slain" — objective text, not a quest
|
||||
}
|
||||
for questId, data in pairs(learned.quests) do
|
||||
local hasRealData = false
|
||||
-- Check if quest has any meaningful data beyond mc/ls metadata
|
||||
if type(data[1]) == "string" and data[1] ~= "" then
|
||||
hasRealData = true
|
||||
end
|
||||
-- Also check for objective data, level, zone, etc.
|
||||
if not hasRealData then
|
||||
for k, v in pairs(data) do
|
||||
if k ~= "mc" and k ~= "ls" then
|
||||
hasRealData = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
local qid = tonumber(questId)
|
||||
if not hasRealData or (qid and OBJECTIVE_TEXT_QUEST_IDS[qid]) then
|
||||
local reason = not hasRealData and "garbage" or "objective-text"
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] Purged", reason, "quest", questId, data[1] or "?")
|
||||
learned.quests[questId] = nil
|
||||
purgedQuests = purgedQuests + 1
|
||||
end
|
||||
end
|
||||
if purgedQuests > 0 then
|
||||
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Purged", purgedQuests, "invalid quests (garbage/objective-text)")
|
||||
end
|
||||
|
||||
-- Infer sortKey [17] from zone data [3] for quests that have a name but no sortKey.
|
||||
-- Field [3] contains zone/area info (e.g. {{470}} for Ghostlands, {{414}} for Zul'Drak).
|
||||
-- The sortKey should be the areaId from [3] so the quest appears in the correct zone in the UI.
|
||||
local sortKeysInferred = 0
|
||||
for questId, data in pairs(learned.quests) do
|
||||
if data[1] and not data[17] and data[3] then
|
||||
-- [3] can be a table of tables: {{414}} or nested {{414, ...}, ...}
|
||||
-- Extract the first numeric areaId from it
|
||||
local sortKey = nil
|
||||
if type(data[3]) == "table" then
|
||||
-- Walk into nested tables to find the first numeric value
|
||||
local function findFirstNumber(t)
|
||||
if type(t) ~= "table" then return nil end
|
||||
for i = 1, #t do
|
||||
if type(t[i]) == "number" then
|
||||
return t[i]
|
||||
elseif type(t[i]) == "table" then
|
||||
local result = findFirstNumber(t[i])
|
||||
if result then return result end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
sortKey = findFirstNumber(data[3])
|
||||
elseif type(data[3]) == "number" then
|
||||
sortKey = data[3]
|
||||
end
|
||||
if sortKey then
|
||||
data[17] = sortKey
|
||||
sortKeysInferred = sortKeysInferred + 1
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] Inferred sortKey", sortKey, "for quest", questId, data[1])
|
||||
end
|
||||
end
|
||||
end
|
||||
if sortKeysInferred > 0 then
|
||||
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Inferred sortKey for", sortKeysInferred, "quests from zone data")
|
||||
end
|
||||
|
||||
-- 2. Quests
|
||||
local questIdsToFix = {}
|
||||
for questId, data in pairs(learned.quests) do
|
||||
@@ -1512,7 +1786,7 @@ function QuestieLearner:OnMouseoverUnit()
|
||||
local level = UnitLevel("mouseover")
|
||||
local zoneText = GetRealZoneText()
|
||||
local areaId = _Learner.zoneCache[zoneText]
|
||||
if not areaId then
|
||||
if not areaId and l10n and l10n.GetAreaIdByLocalName then
|
||||
areaId = l10n:GetAreaIdByLocalName(zoneText)
|
||||
if areaId then
|
||||
_Learner.zoneCache[zoneText] = areaId
|
||||
@@ -1707,7 +1981,7 @@ function QuestieLearner:OnQuestAccepted(firstArg, secondArg)
|
||||
|
||||
-- Zone: reverse-lookup from GetRealZoneText() which is always accurate on 3.3.5.
|
||||
local zoneText = GetRealZoneText()
|
||||
if zoneText and zoneText ~= "" then
|
||||
if zoneText and zoneText ~= "" and l10n and l10n.GetAreaIdByLocalName then
|
||||
local areaId = l10n:GetAreaIdByLocalName(zoneText)
|
||||
if areaId and areaId > 0 then
|
||||
data[17] = areaId
|
||||
@@ -1973,12 +2247,27 @@ function QuestieLearner:OnCombatLogEvent(...)
|
||||
if not dstGUID then return end
|
||||
|
||||
local npcId = GetNpcIdFromGUID(dstGUID)
|
||||
local name = dstName
|
||||
|
||||
-- Fallback chain for mob name: combat-log dstName → cached target/mouseover → current target unit
|
||||
if not name or name == "" then
|
||||
if _Learner.guidNpcCache then
|
||||
local cached = _Learner.guidNpcCache[dstGUID]
|
||||
if cached and cached.name and cached.name ~= "" then
|
||||
name = cached.name
|
||||
end
|
||||
end
|
||||
end
|
||||
if not name or name == "" then
|
||||
if UnitGUID("target") == dstGUID then
|
||||
name = UnitName("target")
|
||||
end
|
||||
end
|
||||
|
||||
if not npcId and _Learner.guidNpcCache then
|
||||
local cached = _Learner.guidNpcCache[dstGUID]
|
||||
if cached then
|
||||
npcId = cached.npcId
|
||||
if not dstName then dstName = cached.name end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1993,7 +2282,7 @@ function QuestieLearner:OnCombatLogEvent(...)
|
||||
local zoneText = GetRealZoneText and GetRealZoneText() or ""
|
||||
_Learner.recentKills[dstGUID] = {
|
||||
npcId = npcId,
|
||||
name = dstName or "",
|
||||
name = name or "",
|
||||
x = px,
|
||||
y = py,
|
||||
zoneId = zoneId,
|
||||
@@ -2005,6 +2294,9 @@ function QuestieLearner:OnCombatLogEvent(...)
|
||||
Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] Kill cached for correlation:", npcId, dstName, "@", tostring(px), tostring(py), "zone", tostring(zoneId))
|
||||
end
|
||||
|
||||
-- Unconditionally map the spawn position for Ascension DB building
|
||||
self:LearnNPC(npcId, name, nil, nil, nil, nil, px, py, zoneId)
|
||||
|
||||
-- TTL cleanup: drop entries older than 10 minutes
|
||||
local now = time()
|
||||
for g, entry in pairs(_Learner.recentKills) do
|
||||
|
||||
@@ -31,22 +31,26 @@ local GetClassColor = QuestieCompat.GetClassColor
|
||||
|
||||
|
||||
-- Silent quest log index lookup (prevents warning spam for auto-complete/achievement "quests")
|
||||
-- Uses QuestieCompat wrapper which normalizes return values across API versions.
|
||||
local function _Questie_SilentGetQuestLogIndexByID(questId)
|
||||
questId = tonumber(questId)
|
||||
if not questId then return 0 end
|
||||
|
||||
if _G.GetQuestLogIndexByID then
|
||||
return _G.GetQuestLogIndexByID(questId) or 0
|
||||
-- QuestieCompat.GetQuestLogIndexByID returns nil when not found,
|
||||
-- coerce to 0 for callers that check > 0.
|
||||
local idx = QuestieCompat.GetQuestLogIndexByID(questId)
|
||||
if idx then
|
||||
return idx
|
||||
end
|
||||
|
||||
local n = (GetNumQuestLogEntries and select(1, GetNumQuestLogEntries())) or 0
|
||||
if n > 0 and GetQuestLogTitle then
|
||||
for i = 1, n do
|
||||
local _, _, _, isHeader, _, _, _, qid = GetQuestLogTitle(i)
|
||||
qid = tonumber(qid)
|
||||
if (not isHeader) and qid and qid == questId then
|
||||
return i
|
||||
end
|
||||
-- Fallback: use the compat-normalized GetQuestLogTitle (always 8 returns)
|
||||
local GetQuestLogTitleCompat = QuestieCompat.GetQuestLogTitle
|
||||
local n = select(1, GetNumQuestLogEntries()) or 0
|
||||
for i = 1, n do
|
||||
local _, _, _, isHeader, _, _, _, qid = GetQuestLogTitleCompat(i)
|
||||
qid = tonumber(qid)
|
||||
if (not isHeader) and qid and qid == questId then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return 0
|
||||
@@ -224,8 +228,14 @@ function MapIconTooltip:Show()
|
||||
handleMapIcon(icon)
|
||||
end
|
||||
else
|
||||
for pin in HBDPins.worldmapProvider:GetMap():EnumeratePinsByTemplate("HereBeDragonsPinsTemplateQuestie") do
|
||||
handleMapIcon(pin.icon)
|
||||
if QuestieCompat.Is335 then
|
||||
for icon, _ in next, HBDPins.worldmapPins do
|
||||
handleMapIcon(icon)
|
||||
end
|
||||
else
|
||||
for pin in HBDPins.worldmapProvider:GetMap():EnumeratePinsByTemplate("HereBeDragonsPinsTemplateQuestie") do
|
||||
handleMapIcon(pin.icon)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -402,7 +412,7 @@ function MapIconTooltip:Show()
|
||||
break
|
||||
end
|
||||
|
||||
factionName = select(1, GetFactionInfoByID(factionId))
|
||||
factionName = select(1, QuestieCompat.GetFactionInfoByID(factionId))
|
||||
if factionName then
|
||||
rewardValue = rewardPair[2]
|
||||
|
||||
@@ -423,10 +433,10 @@ function MapIconTooltip:Show()
|
||||
end
|
||||
|
||||
if aldorPenalty then
|
||||
factionName = select(1, GetFactionInfoByID(932))
|
||||
factionName = select(1, QuestieCompat.GetFactionInfoByID(932))
|
||||
tinsert(rewardTable, aldorPenalty .. " " .. factionName)
|
||||
elseif scryersPenalty then
|
||||
factionName = select(1, GetFactionInfoByID(934))
|
||||
factionName = select(1, QuestieCompat.GetFactionInfoByID(934))
|
||||
tinsert(rewardTable, scryersPenalty .. " " .. factionName)
|
||||
end
|
||||
|
||||
@@ -513,6 +523,7 @@ function MapIconTooltip:Show()
|
||||
end
|
||||
return levelString
|
||||
end
|
||||
|
||||
-- Used to get the white color for the quests which don't have anything to collect
|
||||
local defaultQuestColor = QuestieLib:GetRGBForObjective({})
|
||||
local creatureLevels = QuestieDB:GetCreatureLevels(quest) -- Data for min and max level
|
||||
@@ -585,7 +596,7 @@ function MapIconTooltip:Show()
|
||||
break
|
||||
end
|
||||
|
||||
factionName = select(1, GetFactionInfoByID(factionId))
|
||||
factionName = select(1, QuestieCompat.GetFactionInfoByID(factionId))
|
||||
if factionName then
|
||||
rewardValue = rewardPair[2]
|
||||
|
||||
@@ -606,10 +617,10 @@ function MapIconTooltip:Show()
|
||||
end
|
||||
|
||||
if aldorPenalty then
|
||||
factionName = select(1, GetFactionInfoByID(932))
|
||||
factionName = select(1, QuestieCompat.GetFactionInfoByID(932))
|
||||
tinsert(rewardTable, aldorPenalty .. " " .. factionName)
|
||||
elseif scryersPenalty then
|
||||
factionName = select(1, GetFactionInfoByID(934))
|
||||
factionName = select(1, QuestieCompat.GetFactionInfoByID(934))
|
||||
tinsert(rewardTable, scryersPenalty .. " " .. factionName)
|
||||
end
|
||||
|
||||
|
||||
@@ -232,7 +232,7 @@ function QuestieTooltips:GetTooltip(key)
|
||||
-- Try to find in learned NPCs or objects
|
||||
local id = tonumber(key:sub(3))
|
||||
if id then
|
||||
if key:sub(1,2) == "m_" then
|
||||
if key:sub(1,2) == "m_" then
|
||||
local learnedNpc = QuestieLearner.data.npcs[id]
|
||||
-- npc[10] from _AddToArray is an array of questIds, not {questId -> objList}
|
||||
if learnedNpc and learnedNpc[10] then
|
||||
@@ -248,13 +248,11 @@ function QuestieTooltips:GetTooltip(key)
|
||||
local objText = objEntry[2]
|
||||
local needed, collected
|
||||
local objectives = QuestLogCache.GetQuestObjectives(questId)
|
||||
local objectiveIcon
|
||||
if objectives then
|
||||
for _, obj in next, objectives do
|
||||
if obj.text and objText and (obj.text == objText or string.find(obj.text, objText, 1, true) or string.find(objText, obj.text, 1, true)) then
|
||||
needed = obj.numRequired
|
||||
collected = obj.numFulfilled
|
||||
objectiveIcon = obj.Icon
|
||||
break
|
||||
end
|
||||
end
|
||||
@@ -264,7 +262,6 @@ function QuestieTooltips:GetTooltip(key)
|
||||
Description = objText,
|
||||
Needed = needed,
|
||||
Collected = collected,
|
||||
Icon = objectiveIcon,
|
||||
Update = function(self)
|
||||
local objs = QuestLogCache.GetQuestObjectives(questId)
|
||||
if objs then
|
||||
@@ -288,7 +285,7 @@ function QuestieTooltips:GetTooltip(key)
|
||||
tinsert(tooltipLines, "|cFF5EBAF3(Learned - Confidence: " .. tostring(learnedNpc.mc) .. ")|r")
|
||||
end
|
||||
end
|
||||
elseif key:sub(1,2) == "o_" then
|
||||
elseif key:sub(1,2) == "o_" then
|
||||
local learnedObj = QuestieLearner.data.objects[id]
|
||||
-- obj[2] from _AddToArray is an array of questIds (questStarts), not {questId -> objList}
|
||||
if learnedObj and learnedObj[2] then
|
||||
@@ -304,13 +301,11 @@ function QuestieTooltips:GetTooltip(key)
|
||||
local objText = objEntry[2]
|
||||
local needed, collected
|
||||
local objectives = QuestLogCache.GetQuestObjectives(questId)
|
||||
local objectiveIcon
|
||||
if objectives then
|
||||
for _, obj in next, objectives do
|
||||
if obj.text and objText and (obj.text == objText or string.find(obj.text, objText, 1, true) or string.find(objText, obj.text, 1, true)) then
|
||||
needed = obj.numRequired
|
||||
collected = obj.numFulfilled
|
||||
objectiveIcon = obj.Icon
|
||||
break
|
||||
end
|
||||
end
|
||||
@@ -320,7 +315,6 @@ function QuestieTooltips:GetTooltip(key)
|
||||
Description = objText,
|
||||
Needed = needed,
|
||||
Collected = collected,
|
||||
Icon = objectiveIcon,
|
||||
Update = function(self)
|
||||
local objs = QuestLogCache.GetQuestObjectives(questId)
|
||||
if objs then
|
||||
|
||||
Reference in New Issue
Block a user