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.
|
- **[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.
|
- **[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
|
### 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.
|
- **[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)
|
local quest = QuestieDB.GetQuest(questId)
|
||||||
|
|
||||||
if quest then
|
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
|
-- 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
|
-- have stale completion state (e.g. complete-then-abandon-then-reaccept leaves
|
||||||
-- quest.isComplete=true, WasComplete=true). Only check quest-object flags
|
-- quest.isComplete=true, WasComplete=true). Only check quest-object flags
|
||||||
@@ -1742,6 +1746,140 @@ _UnloadAlreadySpawnedIcons = function(objective)
|
|||||||
end
|
end
|
||||||
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 quest Quest
|
||||||
---@param objective QuestObjective
|
---@param objective QuestObjective
|
||||||
---@param objectiveIndex ObjectiveIndex
|
---@param objectiveIndex ObjectiveIndex
|
||||||
@@ -1792,7 +1930,10 @@ _DetermineIconsToDraw = function(quest, objective, objectiveIndex, objectiveCent
|
|||||||
local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone)
|
local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone)
|
||||||
local _, spawn = next(spawns)
|
local _, spawn = next(spawns)
|
||||||
while _ do
|
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 = {
|
local drawIcon = {
|
||||||
AlreadySpawnedId = id,
|
AlreadySpawnedId = id,
|
||||||
data = data,
|
data = data,
|
||||||
|
|||||||
@@ -181,10 +181,17 @@ function QuestieEventHandler:RegisterLateEvents()
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Questie Debug Offer
|
-- LOOT_OPENED: remove pins for looted quest object nodes, and (SoD) run the debug offer.
|
||||||
if Questie.IsSoD then
|
-- AceEvent allows only one handler per event on the Questie object, so both run here.
|
||||||
Questie:RegisterEvent("LOOT_OPENED", QuestieDebugOffer.LootWindow)
|
Questie:RegisterEvent("LOOT_OPENED", function(...)
|
||||||
end
|
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
|
-- Questie Comms Events
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user