feat(map): remove looted object node pins on loot
When a quest object node is opened/looted, that specific node's map/minimap pin now disappears immediately even before the objective is fully collected, instead of all node pins staying until turn-in. On LOOT_OPENED, QuestieQuest:RemoveLootedObjectivePins finds the active object-objective spawn nearest the player's world position (<=12 yd), marks it consumed in Questie.db.char.lootedObjectSpawns, and repopulates just that objective so clustering recomputes with the node skipped. _DetermineIconsToDraw skips consumed object spawns, so removal persists across redraws and /reload. ClearLootedSpawns runs on AcceptQuest, so abandoning and re-doing the quest (including after an Ascension prestige) shows every node again. Works in every data source mode (auto/learner/static/none): suppression is centralized in the shared _DetermineIconsToDraw that all redraw routes funnel through, operating on the mode-resolved objective.spawnList. Matching is radius-based (1.5 zone units) so a learner spawn re-added at the player's position for the just-looted node is suppressed too. Scoped to object-type objectives; monster/kill pins are unchanged.
This commit is contained in:
@@ -20,6 +20,10 @@
|
||||
- **[Measured Hot Paths - Phase 3]** Landed measured optimizations on the phase 3 branch for literal localization caching, available quest redraw batching, `QuestieDB.IsDoable` batch reads, hot profile aliases, `GetTime()` hoists, NPC fallback lookup caching, and validate-cache allocation cleanup.
|
||||
- **[Localization - Zero-Arg Translate Fast Path]** `_l10n:translate` now short-circuits the common no-argument case (`select("#", ...) == 0`) and returns the translation directly, skipping the `{...}` table allocation, the `tostring` coercion loop, and the `safeFormat` call. The vast majority of translation lookups pass no format arguments, so this avoids per-call garbage on a hot path. Behavior matches the existing slow path: a successful no-arg lookup already returned the raw value, and the missing/invalid-entry branches fall back to the key. Cherry-picked from phase2-lua50-sweep.
|
||||
|
||||
### Features
|
||||
|
||||
- **[Map - Looted Object Pins Disappear On Loot]** When you open/loot a quest object node (e.g. Fell Wood piles), that specific node's map/minimap pin now disappears immediately even if the objective is not yet fully collected, instead of all node pins staying until turn-in. On `LOOT_OPENED`, `QuestieQuest:RemoveLootedObjectivePins` finds the active object-objective spawn nearest the player's world position (within ~12 yards, since you stand on the node to loot it), records it as consumed in `Questie.db.char.lootedObjectSpawns`, and redraws just that objective so clustering recomputes with the looted node skipped. `_DetermineIconsToDraw` skips consumed object spawns, so removal persists across redraws and `/reload`. The looted history is cleared on quest accept (`QuestieQuest:ClearLootedSpawns`), so abandoning and re-doing the quest — including after an Ascension prestige — shows every node again. Scoped to `object`-type objectives; monster/kill pins are unchanged. Works in every data source mode (auto / learner / static / none): the suppression is centralized in the single shared `_DetermineIconsToDraw` draw path that all redraw routes funnel through (core `UpdateQuest`, the learner's debounced `_DoFlushActiveQuestPins`, slider redraws), and it operates on the already mode-resolved `objective.spawnList`. Matching is radius-based (1.5 zone units) rather than exact-coordinate so that in learner/auto mode a spawn the learner re-adds at the player's position for the just-looted node is also suppressed instead of re-appearing.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **[Map - Completed Objective Pins Linger]** Added `QuestieMap:UnloadQuestFramesForObjective(questId, objectiveIndex)` and call it from every completion path in `QuestieQuest:PopulateObjective` (objective completed, hidden by `ShouldHideObjective`, and static no-`Update` objectives). Previously, removing a completed objective's map/minimap pins relied solely on `_UnloadAlreadySpawnedIcons`, which walks `objective.AlreadySpawned`. That table desyncs from the frames actually on screen after learner spawn-list invalidation or complete→abandon→reaccept cycles, so when it was reset to `{}` the live frames leaked and pins for fully-completed objectives (e.g. 6/6 kills, a collected item objective) stayed on the map even though tooltips updated correctly. The new function unloads frames deterministically off the map's own `questIdFrames` registry by matching `frame.data.ObjectiveIndex`, so completed-objective pins are removed regardless of `AlreadySpawned` state. Restricted to positive standard-objective indices; SpecialObjectives (sentinel index 0) keep using `AlreadySpawned` to avoid cross-unloading siblings.
|
||||
|
||||
@@ -496,6 +496,10 @@ function QuestieQuest:AcceptQuest(questId)
|
||||
local quest = QuestieDB.GetQuest(questId)
|
||||
|
||||
if quest then
|
||||
-- Fresh acceptance: clear any looted-object-spawn history so every node pin
|
||||
-- shows again (covers re-doing the quest after abandon or an Ascension prestige).
|
||||
QuestieQuest:ClearLootedSpawns(questId)
|
||||
|
||||
-- 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
|
||||
@@ -1742,6 +1746,140 @@ _UnloadAlreadySpawnedIcons = function(objective)
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Looted object-spawn tracking
|
||||
-- When a player loots/opens a quest object node, that specific node's pin should
|
||||
-- disappear even if the whole objective is not yet complete. Consumed node
|
||||
-- positions are persisted per quest+objective in char DB so the pin stays gone
|
||||
-- across reloads, and are cleared on (re)accept so abandoning + redoing the quest
|
||||
-- (e.g. after an Ascension prestige) starts fresh.
|
||||
--
|
||||
-- Matching is RADIUS-based, not exact-coordinate, and works across every data
|
||||
-- source mode (auto / learner / static / none): the suppression operates on the
|
||||
-- already-resolved objective.spawnList, and the radius absorbs the small coordinate
|
||||
-- differences between the static DB spawn and a learner spawn that QuestieLearner
|
||||
-- may add at the player's position for the very node that was just looted.
|
||||
-- ============================================================================
|
||||
|
||||
-- Suppression radius in 0-100 zone units. Wide enough to cover DB-vs-learner
|
||||
-- coordinate drift (learner dedups within spawnDedupRadius), tight enough not to
|
||||
-- swallow neighbouring nodes in a dense gather field.
|
||||
local LOOTED_SPAWN_RADIUS = 1.5
|
||||
|
||||
local function _GetLootedStore(create)
|
||||
if (not Questie.db) or (not Questie.db.char) then return nil end
|
||||
local store = Questie.db.char.lootedObjectSpawns
|
||||
if (not store) and create then
|
||||
store = {}
|
||||
Questie.db.char.lootedObjectSpawns = store
|
||||
end
|
||||
return store
|
||||
end
|
||||
|
||||
---True if (zone,x,y) is within LOOTED_SPAWN_RADIUS of any node already looted for
|
||||
---this objective. Mode-agnostic: callers pass coordinates straight from the active
|
||||
---objective.spawnList.
|
||||
---@param questId number
|
||||
---@param objectiveIndex ObjectiveIndex
|
||||
function QuestieQuest:IsSpawnLooted(questId, objectiveIndex, zone, x, y)
|
||||
local store = _GetLootedStore(false)
|
||||
if not store then return false end
|
||||
local q = store[questId]
|
||||
if not q then return false end
|
||||
local points = q[objectiveIndex]
|
||||
if not points then return false end
|
||||
for _, p in pairs(points) do
|
||||
if p.zone == zone then
|
||||
local dx, dy = p.x - x, p.y - y
|
||||
if (dx * dx + dy * dy) <= (LOOTED_SPAWN_RADIUS * LOOTED_SPAWN_RADIUS) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---@param questId number
|
||||
---@param objectiveIndex ObjectiveIndex
|
||||
function QuestieQuest:MarkObjectSpawnLooted(questId, objectiveIndex, zone, x, y)
|
||||
if QuestieQuest:IsSpawnLooted(questId, objectiveIndex, zone, x, y) then return end
|
||||
local store = _GetLootedStore(true)
|
||||
if not store then return end
|
||||
store[questId] = store[questId] or {}
|
||||
store[questId][objectiveIndex] = store[questId][objectiveIndex] or {}
|
||||
table.insert(store[questId][objectiveIndex], { zone = zone, x = x, y = y })
|
||||
end
|
||||
|
||||
---Clear all looted-spawn history for a quest. Called on accept so re-taking the
|
||||
---quest (after abandon / prestige) shows every node again.
|
||||
---@param questId number
|
||||
function QuestieQuest:ClearLootedSpawns(questId)
|
||||
local store = _GetLootedStore(false)
|
||||
if store then store[questId] = nil end
|
||||
end
|
||||
|
||||
---Fires on LOOT_OPENED. Finds the active object-objective spawn nearest the player
|
||||
---(who is standing on the node being looted), marks it consumed, and redraws just
|
||||
---that objective so the node's pin is removed while the rest stay clustered correctly.
|
||||
---Operates on objective.spawnList, so it is correct in every data source mode; the
|
||||
---radius-based suppression also absorbs a learner spawn re-added for the looted node.
|
||||
function QuestieQuest:RemoveLootedObjectivePins()
|
||||
if not Questie.db.profile.enableObjectives then return end
|
||||
if (not QuestiePlayer) or (not QuestiePlayer.currentQuestlog) then return end
|
||||
if (not HBD) or (not HBD.GetPlayerWorldPosition) then return end
|
||||
|
||||
local px, py, pInstance = HBD:GetPlayerWorldPosition()
|
||||
if (not px) or (not py) then return end
|
||||
|
||||
local TOLERANCE = 12 -- world yards; the player stands on the node when looting it
|
||||
|
||||
for questId in pairs(QuestiePlayer.currentQuestlog) do
|
||||
local quest = QuestieDB.GetQuest(questId)
|
||||
if quest and quest.Objectives then
|
||||
for objectiveIndex, objective in pairs(quest.Objectives) do
|
||||
if objective.Type == "object" and objective.spawnList and (not objective.Completed) then
|
||||
local bestDist, bestZone, bestX, bestY
|
||||
for _, spawnData in pairs(objective.spawnList) do
|
||||
if spawnData.Spawns then
|
||||
for zone, coords in pairs(spawnData.Spawns) do
|
||||
local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone)
|
||||
for _, coord in pairs(coords) do
|
||||
if coord[1] and coord[2] and uiMapId then
|
||||
local wx, wy, wInstance = HBD:GetWorldCoordinatesFromZone(coord[1] / 100, coord[2] / 100, uiMapId)
|
||||
if wx and wy and ((not wInstance) or (not pInstance) or wInstance == pInstance) then
|
||||
local dx, dy = px - wx, py - wy
|
||||
local dist = math.sqrt(dx * dx + dy * dy)
|
||||
if (not bestDist) or dist < bestDist then
|
||||
bestDist, bestZone, bestX, bestY = dist, zone, coord[1], coord[2]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if bestDist and bestDist <= TOLERANCE and
|
||||
not QuestieQuest:IsSpawnLooted(questId, objectiveIndex, bestZone, bestX, bestY) then
|
||||
QuestieQuest:MarkObjectSpawnLooted(questId, objectiveIndex, bestZone, bestX, bestY)
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP,
|
||||
"[QuestieQuest:RemoveLootedObjectivePins] Removed looted object pin quest:", questId,
|
||||
"obj:", objectiveIndex, "zone:", bestZone, "dist:", bestDist)
|
||||
-- Redraw just this objective so clustering recomputes with the
|
||||
-- looted node skipped (see the IsSpawnLooted guard in _DetermineIconsToDraw).
|
||||
QuestieMap:UnloadQuestFramesForObjective(questId, objectiveIndex)
|
||||
objective.AlreadySpawned = {}
|
||||
local ok, err = xpcall(QuestieQuest.PopulateObjective, ERR_FUNCTION, QuestieQuest, quest, objectiveIndex, objective, false)
|
||||
if not ok then
|
||||
Questie:Debug(Questie.DEBUG_ELEVATED, "[QuestieQuest:RemoveLootedObjectivePins] PopulateObjective error:", err)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param quest Quest
|
||||
---@param objective QuestObjective
|
||||
---@param objectiveIndex ObjectiveIndex
|
||||
@@ -1792,7 +1930,10 @@ _DetermineIconsToDraw = function(quest, objective, objectiveIndex, objectiveCent
|
||||
local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone)
|
||||
local _, spawn = next(spawns)
|
||||
while _ do
|
||||
if (spawn[1] and spawn[2]) then
|
||||
-- Skip object spawns the player has already looted this quest cycle so
|
||||
-- the consumed node's pin stays removed across redraws/reloads.
|
||||
if (spawn[1] and spawn[2]) and
|
||||
not (objective.Type == "object" and QuestieQuest:IsSpawnLooted(quest.Id, objectiveIndex, zone, spawn[1], spawn[2])) then
|
||||
local drawIcon = {
|
||||
AlreadySpawnedId = id,
|
||||
data = data,
|
||||
|
||||
@@ -181,10 +181,17 @@ function QuestieEventHandler:RegisterLateEvents()
|
||||
end)
|
||||
end
|
||||
|
||||
-- Questie Debug Offer
|
||||
if Questie.IsSoD then
|
||||
Questie:RegisterEvent("LOOT_OPENED", QuestieDebugOffer.LootWindow)
|
||||
end
|
||||
-- LOOT_OPENED: remove pins for looted quest object nodes, and (SoD) run the debug offer.
|
||||
-- AceEvent allows only one handler per event on the Questie object, so both run here.
|
||||
Questie:RegisterEvent("LOOT_OPENED", function(...)
|
||||
local ok, err = pcall(function() QuestieQuest:RemoveLootedObjectivePins() end)
|
||||
if not ok then
|
||||
Questie:Debug(Questie.DEBUG_ELEVATED, "[QuestieEventHandler] RemoveLootedObjectivePins error:", err)
|
||||
end
|
||||
if Questie.IsSoD and QuestieDebugOffer and QuestieDebugOffer.LootWindow then
|
||||
QuestieDebugOffer.LootWindow(...)
|
||||
end
|
||||
end)
|
||||
|
||||
-- Questie Comms Events
|
||||
|
||||
|
||||
Reference in New Issue
Block a user