view QuestPOI.lua @ 113:03e4a8b93012 v7.3.0-2

7.3 Updates - Added some map frame adjustments to keep the filter bar from obstructing edge of zone clicks KNOWN ISSUES: - Argus quests can only be filtered when viewing Argus maps - Main Argus map in the WorldMapFrame has misaligned pins; this is due to it using a different map texture from what is used in the teleporter map, so all the POI coordinates are based on that map in the client info
author Nenue
date Tue, 05 Sep 2017 03:14:34 -0400
parents e8b6c5433128
children bf4a36378bb9
line wrap: on
line source
-- WorldPlan
-- QuestPOI.lua
-- Created: 10/1/2016 7:21 PM
-- %file-revision%
-- Big TODOs:
-- -- generate frames using `WorldMap_GetOrCreateTaskPOI' to remove tainting issues
--
local _, db = ...
local TQ_GetQuestInfoByQuestID = C_TaskQuest.GetQuestInfoByQuestID
local TQ_GetQuestLocation = C_TaskQuest.GetQuestLocation
local TQ_GetQuestTimeLeftMinutes = C_TaskQuest.GetQuestTimeLeftMinutes
local TQ_IsActive = C_TaskQuest.IsActive
local TQ_RequestPreloadRewardData = C_TaskQuest.RequestPreloadRewardData
local QuestPOIGetIconInfo, WorldMapPOIFrame =  QuestPOIGetIconInfo, WorldMapPOIFrame
local WorldMap_DoesWorldQuestInfoPassFilters = WorldMap_DoesWorldQuestInfoPassFilters
local QuestMapFrame_IsQuestWorldQuest = QuestMapFrame_IsQuestWorldQuest
local GetAchievementNumCriteria, GetAchievementCriteriaInfo, GetAchievementInfo = GetAchievementNumCriteria, GetAchievementCriteriaInfo, GetAchievementInfo

local IsQuestFlaggedCompleted = IsQuestFlaggedCompleted
local GetFactionInfoByID, GetQuestObjectiveInfo = GetFactionInfoByID, GetQuestObjectiveInfo
local GetQuestTagInfo, GetProfessionInfo = GetQuestTagInfo, GetProfessionInfo
local GetNumQuestLogRewards, GetNumQuestLogRewardCurrencies, HaveQuestData = GetNumQuestLogRewards, GetNumQuestLogRewardCurrencies, HaveQuestData
local GetQuestLogRewardMoney, GetQuestLogRewardCurrencyInfo, GetMoneyString = GetQuestLogRewardMoney, GetQuestLogRewardCurrencyInfo, GetMoneyString
local SpellCanTargetQuest, GetCVarBool = SpellCanTargetQuest, GetCVarBool
local SetSuperTrackedQuestID = SetSuperTrackedQuestID
local HaveQuestRewardData = HaveQuestRewardData
local GetTime = GetTime

local totalPins = 0
local completedQuests = {}

local pairs, ipairs, tinsert, tremove, unpack, select = pairs, ipairs, tinsert, tremove, unpack, select
local floor, mod, tostring, tonumber, GetSuperTrackedQuestID = floor, mod, tostring, tonumber, GetSuperTrackedQuestID
local GameTooltip = GameTooltip
local GetItemIcon = GetItemIcon

local print = DEVIAN_WORKSPACE and function(...) _G.print('POI', ...) end or nop
local wprint = DEVIAN_WORKSPACE and function(...) _G.print('WP', ...) end or nop
local wqprint = DEVIAN_WORKSPACE and function(...) _G.print('WorldQuests', ...) end or nop
local iprint = DEVIAN_WORKSPACE and function(...) _G.print('ItemScan', ...) end or nop
local rprint = DEVIAN_WORKSPACE and function(...) _G.print('WQRefresh', ...) end or nop
local dprint = DEVIAN_WORKSPACE and function(...) _G.print('WQData', ...) end or nop
local QuestPOI = WorldPlanPOIMixin

local pinBaseIndex = 1400
local overlayBaseIndex = 1450
local previousHighlight

local FADE_TIMING_MULTIPLIER = 3
local DATA_DEBUG = false
local PIN_REFRESH_DELAY = 1
local PIN_REQUEST_DELAY = .2
local ICON_UNKNOWN = "Interface\\ICONS\\inv_misc_questionmark"
local ICON_MONEY = "Interface\\Buttons\\UI-GroupLoot-Coin-Up"

local WORLD_QUEST_BORDER = "Interface\\UNITPOWERBARALT\\Generic1Target_Circular_Frame"
local PENDING_BORDER = "Interface\\BUTTONS\\YELLOWORANGE64"
local PENDING_ICON = "Interface\\BUTTONS\\YELLOWORANGE64"

local REWARD_CASH = WORLD_QUEST_REWARD_TYPE_FLAG_GOLD
local REWARD_ARTIFACT_POWER = WORLD_QUEST_REWARD_TYPE_FLAG_ARTIFACT_POWER
local REWARD_GEAR = WORLD_QUEST_REWARD_TYPE_FLAG_EQUIPMENT
local REWARD_CURRENCY = WORLD_QUEST_REWARD_TYPE_FLAG_ORDER_RESOURCES
local REWARD_REAGENT = WORLD_QUEST_REWARD_TYPE_FLAG_MATERIALS

local REWARD_TYPE_NAMES = {
  [REWARD_CASH] = 'Money',
  [REWARD_ARTIFACT_POWER] = 'AP',
  [REWARD_GEAR] = 'Equipment',
  [REWARD_CURRENCY] = 'Currency',
  [REWARD_REAGENT] = 'Trade Goods'
}

local LE_QUEST_TAG_TYPE_PVP = LE_QUEST_TAG_TYPE_PVP
local LE_QUEST_TAG_TYPE_PET_BATTLE = LE_QUEST_TAG_TYPE_PET_BATTLE
local LE_QUEST_TAG_TYPE_DUNGEON = LE_QUEST_TAG_TYPE_DUNGEON
local LE_QUEST_TAG_TYPE_PROFESSION = LE_QUEST_TAG_TYPE_PROFESSION
local LE_QUEST_TAG_TYPE_NORMAL = LE_QUEST_TAG_TYPE_NORMAL

local LE_QUEST_TAG_TYPE_PVP = LE_QUEST_TAG_TYPE_PVP
local LE_QUEST_TAG_TYPE_PET_BATTLE = LE_QUEST_TAG_TYPE_PET_BATTLE
local LE_QUEST_TAG_TYPE_DUNGEON = LE_QUEST_TAG_TYPE_DUNGEON
local LE_QUEST_TAG_TYPE_PROFESSION = LE_QUEST_TAG_TYPE_PROFESSION
local LE_QUEST_TAG_TYPE_NORMAL = LE_QUEST_TAG_TYPE_NORMAL

local DEFAULT_STYLE = {
  border = {1,1,1},
  textColor = {1,1,1,1},
  x = 0, y = 0,
  desaturated = true,
  iconMask = "Interface\\Minimap\\UI-Minimap-Background",
  borderMask = "Interface\\Minimap\\UI-Minimap-Background",
  rewardMark = "Interface\\Minimap\\UI-Minimap-Background",
  scaleFactors = {0.25, 0.7, 1},
  iconWidth = 18,
  borderWidth = 2,
  highlightWidth = 2,
  TagSize = 8,
  maxAlertLevel = 3,
  numberFontObject = 'WorldPlanFont',
  showTime = true,
}
local MINIMIZED_STYLE = {
  hideNumber = true,
  hideIcon = true,
  showTime = false,
  iconWidth = 3,
  maxAlertLevel = 0,
}
local REWARD_TYPE_STYLES = {
  [REWARD_CASH] = {
    hideNumber = true,
  },
  [REWARD_ARTIFACT_POWER] = {},
  [REWARD_GEAR] = {
    hideNumber = true,
  },
  [REWARD_CURRENCY] = {
    textColor = {0,1,1,.75}
  },
  [REWARD_REAGENT] = {

    hideNumber = true,
  },
}
local BORDER_SELECTED_BOUNTY = {1, 1, 0, 1 }
local BORDER_CRITERIA = {.25, .5, 1, 1}

local LE_ITEM_CLASS_WEAPON, LE_ITEM_CLASS_ARMOR, LE_ITEM_CLASS_GEM, LE_ITEM_GEM_ARTIFACTRELIC, LE_ITEM_CLASS_TRADEGOODS = LE_ITEM_CLASS_WEAPON, LE_ITEM_CLASS_ARMOR, LE_ITEM_CLASS_GEM, LE_ITEM_GEM_ARTIFACTRELIC, LE_ITEM_CLASS_TRADEGOODS


local STYLE_TYPE_PENDING = 768


-- Pin color/display variables
db.TooltipExtras = db.TooltipExtras or {} -- idiot-proofing
db.PinAlpha = 1

local familiars = {
    [42159] = 'Nightwatcher Merayl',
    [40277] = 'Tiffany Nelson',
    [40298] = 'Sir Galveston',
    [40282] = 'Grixis Tinypop',
    [40278] = 'Robert Craig',
    [48195] = 'Aulier',
    [41990] = 'Varenne',
    [41860] = 'Xorvasc',
    [40299] = 'Bodhi Sunwayver',
    [42442] = 'Amalia',
    [40280] = 'Bredda Tenderhide',
    [41687] = 'Odrogg',
    [41944] = 'Trapper Jarrun',
    [40337] = 'Master Tamer Flummox',
    [40279] = 'Durian Strongfruit'
}
local falcosaurs = {
  [44895] = {44881, 'Sharptalon Hatchling', 115786},
  [44894] = {44882, 'Bloodgazer Hatchling', 115787},
  [44893] = {44880, 'Direbeak Hatchling', 115785},
  [44892] = {44879, 'Snowfeather Hatchling', 115784},
}
local familiars_id = 9696
for questID, name in pairs(familiars) do
  db.TooltipExtras[questID] = {{
    achievementID = familiars_id,
    name = name
  }}
end
for questID, info in pairs(falcosaurs) do
  local trackingQuestID, petName, petID = unpack(info)

  db.TooltipExtras[questID] = {{
    questID = trackingQuestID,
    pet = petName,
    petID = petID
  }}
end


do
  local timeStates = {
    {maxSeconds = 60,
      r=1, g=0.25, b =0, format = function (minutes) return '|cFFFF4400'.. minutes .. 'm' end,
    },
    {maxSeconds = 240,
      r=1, g=.5, b=0, format = function(minutes) return '|cFFFF4400'.. floor(minutes/60) .. 'h' end,
    },
    {maxSeconds = 1440,
      r=1, g=1, b=0, format = function(minutes) return '|cFFFFFF00'.. floor(minutes/60) .. 'h' end,
    },
    {maxSeconds = 10081,
      r=0, g=1, b=0,
    }, -- 7 days + 1 minute
  }
  -- Generates a timeleft string
  function QuestPOI:GetTimeInfo(timeLeft, limit)
    for index = 1, limit do
      local state = timeStates[index]
      if timeLeft <= state.maxSeconds then
        local text
        if state.format then
          text = state.format(timeLeft)
        end
        return text, index, state
      end
    end
    return
  end
end


local GetAchievementTooltipExtras = function(info)

  local hasInfo
  local achievementID = info.achievementID
  local _, name, _, completed, _, _, _, _, _, icon = GetAchievementInfo(achievementID)
  if not completed then

    local numItems = GetAchievementNumCriteria(achievementID)
    local numNeeded = 0
    local tooltipLines = {}
    for i =1, numItems do
      local criteriaName, criteriaType, completed, _, _, _, _, subAchievementID = GetAchievementCriteriaInfo(achievementID, i)
      --print(GetAchievementCriteriaInfo(achievementID, i))

      if not completed then
        --print('::', criteriaName, completed, subAchievementID)
        if criteriaType == 8 then
          local _, _, _, completed, _, _, _, _, _, subIcon = GetAchievementInfo(subAchievementID)
          --print(' -', criteriaName, completed, subIcon)
          if not completed then
            local numCompleted = 0
            local numSubItems = GetAchievementNumCriteria(subAchievementID)
            local subCriteriaLine
            for j = 1, numSubItems do
              local subName, _, completed = GetAchievementCriteriaInfo(subAchievementID, j)

              --print('   -',subName, completed)
              if completed then
                numCompleted = numCompleted + 1
              else
                numNeeded = numNeeded + 1
                if subName:match(info.name) then
                  hasInfo = true
                  subCriteriaLine = '  |T'..subIcon..':16:16|t ' .. criteriaName
                end
              end

            end
            if subCriteriaLine then
              tinsert(tooltipLines, subCriteriaLine .. ' ('..numCompleted..'/'..numSubItems..')')
            end
          end
        elseif criteriaName:match(info.name) and (not completed) then
          numNeeded = numNeeded + 1
          tinsert(tooltipLines, criteriaName)
        end
      end
    end
    if hasInfo then
      WorldMapTooltip:AddLine(" ")
      WorldMapTooltip:AddLine("Achievements:")
      WorldMapTooltip:AddLine(' |T'..icon..':20:20|t '..name)
      for i, line in ipairs(tooltipLines) do
        WorldMapTooltip:AddLine(line)
      end
    end
  end
  return hasInfo
end

local GetQuestTooltipExtras = function(info)
  local questID = info.questID
  local hasInfo

  if info.pet then
    local index, guid = C_PetJournal.FindPetIDByName(info.pet)
    if not index then
      hasInfo = true
      WorldMapTooltip:AddLine('Pets:')
      WorldMapTooltip:AddLine(' - ' .. info.petName)

      if not IsQuestFlaggedCompleted(questID) then
        WorldMapTooltip:AddLine('  Required Quest Flags', 1, 1, 0)
      else
        WorldMapTooltip:AddLine('  Quest Flags Complete!', 0, 1, 0)
      end

    end
  end

end

function QuestPOI:OnLoad()
  --print('|cFF00FF88'..self:GetName()..':OnLoad()|r',db.Config)
  self.debugTimer = 4
  self.title = '|cFF0088FF' .. RETRIEVING_DATA..'|r'
  self.count = self.Overlay.count
  self.timeLabel = self.Overlay.timeLabel
  self.Description = self.Overlay.Description
  self.updateRate = PIN_REQUEST_DELAY
  self.itemName = '|cFF0088FF' .. RETRIEVING_DATA..'|r'
  self.checkFilters = true
  self.checkCriteria = true
  self.checkCursor = true
  self.worldQuest = true
  self.isNew = true

  self.IconBackdrop:SetVertexColor(0,0,0,1)
  self.Overlay:SetPoint('TOPLEFT', self, 'TOPLEFT', 0, 4)
  self.Overlay:SetPoint('BOTTOMRIGHT', self, 'BOTTOMRIGHT', 0, -4)
end

function QuestPOI:OnShow ()
  if self.isStale then
    --print('|cFF00FF00refresh on show')
    self:Refresh('POI_ONSHOW_STALE')
  end

  if self.questID and IsQuestComplete(self.questID) then
    self:Release()
    return
  end

  self:RegisterEvent('QUEST_LOG_UPDATE')
  --self:RegisterEvent('WORLD_MAP_UPDATE')
  self:HideOrShowFrames(true)
end

function QuestPOI:OnEvent(event, questID)
  print(self:GetID()..':OnEvent()', event, questID)
  if (self.questID == questID) and IsQuestComplete(self.questID) then
    db.log(event .. ' for ' .. self:GetName().. ' triggered.')
    self:Release()
    return
  end
  if event == 'WORLD_MAP_UPDATE' then
    self.coordsDirty = true
  end
end

function QuestPOI:OnHide()
  --DEFAULT_CHAT_FRAME:AddMessage('|cFFFFFF00'..self:GetName()..'|r:OnHide()')
  self:HideOrShowFrames(false)
  -- reset flags
  self.icon:SetAlpha(db.PinAlpha)
  self.RewardBorder:SetAlpha(db.PinAlpha)
  self.isAnimating = nil
  --if db.Config.DebugEnabled then
  --  db.log(tostring(self.questID) .. ' ' .. tostring(self.title) .. "\n" .. tostring(REWARD_TYPE_NAMES[self.rewardType]) .. ' ' .. tostring(self.itemName) .. ' ' .. tostring(self.itemNumber) .. "\n|cFFFF4400" .. (self.hideReason or 'NO_MESSAGE') .. "|r\n|cFF00FFFF" ..  debugstack(2,3,0) .. '|r')
  --end
  self.hideReason = nil
end

function QuestPOI:OnEnter()
  if self.filtered and (self.questID ~= GetSuperTrackedQuestID()) then
    return
  end
  WorldMap_HijackTooltip(self.owningFrame);
  self:SetFrameLevel(pinBaseIndex+100)
  self.Overlay:SetFrameLevel(overlayBaseIndex+100)

  if previousHighlight then
    previousHighlight:SetFrameLevel(pinBaseIndex+previousHighlight:GetID())
    previousHighlight.Overlay:SetFrameLevel(overlayBaseIndex+previousHighlight:GetID())
  end

  previousHighlight = self


  WorldMapTooltip:SetOwner(self, "ANCHOR_RIGHT");
  --print('doing tooltip stuff')

  -- Can't add stuff after, so most of the blizzard tooltip hook is simply copied over
  local questID = self.questID
  local color = WORLD_QUEST_QUALITY_COLORS[self.rarity] or NORMAL_FONT_COLOR;


  WorldMapTooltip:SetText(self.title, color.r, color.g, color.b);
  QuestUtils_AddQuestTypeToTooltip(WorldMapTooltip, questID, NORMAL_FONT_COLOR);

  if ( self.factionID ) then
    local factionName = GetFactionInfoByID(self.factionID);
    if ( factionName ) then
      if (self.capped) then
        WorldMapTooltip:AddLine(factionName, GRAY_FONT_COLOR:GetRGB());
      else
        WorldMapTooltip:AddLine(factionName);
      end
    end
  end

  if self.worldQuest then
    WorldMap_AddQuestTimeToTooltip(questID);
  end


  for objectiveIndex = 1, self.numObjectives do
    local objectiveText, objectiveType, finished = GetQuestObjectiveInfo(questID, objectiveIndex, false);
    if ( objectiveText and #objectiveText > 0 ) then
      local color = finished and GRAY_FONT_COLOR or HIGHLIGHT_FONT_COLOR;
      WorldMapTooltip:AddLine(QUEST_DASH .. objectiveText, color.r, color.g, color.b, true);
    end
  end

  local percent = C_TaskQuest.GetQuestProgressBarInfo(self.questID);
  if ( percent ) then
    GameTooltip_InsertFrame(WorldMapTooltip, WorldMapTaskTooltipStatusBar);
    WorldMapTaskTooltipStatusBar.Bar:SetValue(percent);
    WorldMapTaskTooltipStatusBar.Bar.Label:SetFormattedText(PERCENTAGE_STRING, percent);
  end

  if db.TooltipExtras[self.questID] then
    for index, info in pairs(db.TooltipExtras[questID]) do
      if info.achievementID then
        GetAchievementTooltipExtras(info)
      end
      if info.questID then
        GetQuestTooltipExtras(info)
      end
    end
  end
  GameTooltip_AddQuestRewardsToTooltip(WorldMapTooltip, questID)

  WorldMapTooltip:Show()
  --WorldMapTooltip.recalculatePadding = true;
  --print(WorldMapTooltip:GetParent())
  --print(WorldMapTooltip:IsVisible())
end

function QuestPOI:OnMouseDown(button)

  if button == 'RightButton' then

    local timers = db.IgnoreTimers
    if timers[self.questID] then
      timers[self.questID] = nil
    else
      local tl = TQ_GetQuestTimeLeftMinutes(self.questID)
      if tl and (tl ~= 0) then
        timers[self.questID] = time() + (tl * 60)
      else
        timers[self.questID] = 0
      end
    end
    self:Refresh()
  else
    TaskPOI_OnClick(self, button)
  end
end

function QuestPOI:OnLeave()
  if self.filtered and (self.questID ~= GetSuperTrackedQuestID()) then
    return
  end
  WorldMap_RestoreTooltip()
  WorldMapTooltip:Hide();
end



local updateTime, markTime
function QuestPOI:OnUpdate (sinceLast)

  if self.toAlpha then
    if not self.alphaStart then
      self.alphaStart = GetTime()
    end


    local alpha = self.icon:GetAlpha()
    local alphaMod = ((GetTime()-self.alphaStart) *FADE_TIMING_MULTIPLIER)

    if alpha > self.toAlpha then
      alpha = alpha - (sinceLast*FADE_TIMING_MULTIPLIER)

      if (alpha <= self.toAlpha) then
        alpha = self.toAlpha
        self.toAlpha = nil
      elseif (alpha <= 0)  then
        alpha = 0
        self.toAlpha = nil
      end
    elseif alpha < self.toAlpha then
      alpha = alpha + alphaMod

      if (alpha >= self.toAlpha) then
        alpha = self.toAlpha
        self.toAlpha = nil
      elseif (alpha >= 1) then
        alpha = 1
        self.toAlpha = nil
      end

    else
      self.toAlpha = nil
      self.alphaStart = nil
    end

    self.icon:SetAlpha(alpha)
    self.RewardBorder:SetAlpha(alpha)
  end

  -- control update check intervals
  self.throttle = (self.throttle or self.updateRate) + sinceLast
  if self.throttle >= self.updateRate then
    -- factor overtime into the throttle timer
    self.throttle = self.throttle - self.updateRate
  else
    return
  end
  --@debug@
  if DATA_DEBUG then
    self.debugTimer = self.debugTimer - sinceLast
    if self.debugTimer >= 0 then
      print(self.debugTimer)
      return
    end
  end
  --@end-debug@

  -- query for reward data if it wasn't found in the original scan

  if not self.dataLoaded then
    local dataLoaded = self:GetData()
    if dataLoaded and not tContains(db.UpdatedPins, self) then
      print('|cFF00FF88  queueing for update')
      self.isNew = true
      self.toAlpha = nil
      self.alphaStart = nil
      self:Refresh()
    end
  end

  if self.isStale then
    self:Refresh()
  elseif self.maxAlertLevel then
    self:UpdateStatus()
  end
end


function QuestPOI:StartFading()
  if not self.toAlpha then
    print('setting toAlpha')
    self.toAlpha = 1
    self.icon:SetAlpha(0)
    self.RewardBorder:SetAlpha(0)
  end
end

-- attempt to pull pin data
function QuestPOI:GetData ()
  --dprint('|cFF00FF88'..self:GetID()..':GetData()|r')
  local questID = self.questID
  if not questID then
    --dprint('|cFFFF4400bad pin|r', self:GetName())
    return nil
  end

  local questTitle, factionID, capped = TQ_GetQuestInfoByQuestID(questID)
  -- if the title is nil, then wait and try later
  if not questTitle then
    --dprint('|cFFBB8844dataLoaded|r = false|cFF00FFFF', self.questId)
    return false
  else
    self.title, self.factionID, self.capped = questTitle, factionID, capped
    --print(questTitle, factionID, capped)
    -- set tag details
    local worldQuestType
    self.tagID, self.tagName, worldQuestType, self.rarity, self.isElite, self.tradeskillLineIndex = GetQuestTagInfo(questID);
    local tagAtlas
    if worldQuestType == LE_QUEST_TAG_TYPE_PET_BATTLE then
      tagAtlas = "worldquest-icon-petbattle"
    elseif worldQuestType == LE_QUEST_TAG_TYPE_PVP then
      tagAtlas = "worldquest-icon-pvp-ffa"
    elseif worldQuestType == LE_QUEST_TAG_TYPE_PROFESSION then
      self.isKnownProfession = nil
      local id = self.tradeskillLineIndex and select(7, GetProfessionInfo(self.tradeskillLineIndex))
      if id then
        self.isKnownProfession = true
        --print('profession' , self.title, id)
        tagAtlas = WORLD_QUEST_ICONS_BY_PROFESSION[id]
      end
    elseif worldQuestType == LE_QUEST_TAG_TYPE_DUNGEON then
      tagAtlas = "worldquest-icon-dungeon"
    end
    self.worldQuestType = worldQuestType
    self.tagAtlas = tagAtlas

    local dataLoaded, rewardType, itemName, itemTexture, itemNumber, quality, xpType, xpName, xpIcon, xpCount = self:UpdateRewards()
    --dprint('|cFFBB8844  dataLoaded|r =', dataLoaded, rewardType, itemName, itemTexture, itemNumber, quality)
    if dataLoaded then
      self.rewardType = rewardType
      self.itemName = itemName
      self.itemTexture = itemTexture
      self.itemNumber = itemNumber
      self.itemQuality = quality
      self.dataLoaded = true
      self.isStale = true
      self.xpType, self.xpName, self.xpIcon, self.xpCount = xpType, xpName, xpIcon, xpCount
      WorldPlan.dataFlush = true
    end
  end


  return self.dataLoaded
end

local shownCurrencies = {
  [1220] = true,
  [1342]  = true,
  [1508] = true, -- argunite
  [1506] = true, -- argus waystone
}

--- Returns true if data has changed (either from loading in or qualifications changed)
function QuestPOI:UpdateRewards()
  local questID = self.questID
  if not HaveQuestRewardData(questID) then
    C_TaskQuest.RequestPreloadRewardData(questID);
    return false;
  else
    local rewardMoney, rewardAP, rewardHonor
    local rewardItems, rewardCurrency = {}, {}
    local rewardIcon, rewardName, rewardCount, rewardStyle, rewardType, itemID, quantity, quality
    local xpIcon, xpName, xpCount, xpType
    local foundPrimary

    if (GetNumQuestLogRewardCurrencies(questID) > 0 or GetNumQuestLogRewards(questID) > 0 or GetQuestLogRewardMoney(questID) > 0 or GetQuestLogRewardArtifactXP(questID) > 0 or GetQuestLogRewardHonor(questID)) then
      local money = GetQuestLogRewardMoney(questID)
      if money > 0 then
        rewardMoney = money
        rewardIcon = ICON_MONEY
        rewardName = GetMoneyString(money)
        rewardCount = floor(money/10000)
        rewardType = REWARD_CASH
      end

      local artifactXP = GetQuestLogRewardArtifactXP(questID);
      if artifactXP > 0 then
        rewardAP = artifactXP
        rewardIcon = "Interface\\ICONS\\inv_7xp_inscription_talenttome01"
        rewardCount = artifactXP
        rewardType = REWARD_ARTIFACT_POWER
        --dprint('  artifactXP', artifactXP)
        foundPrimary = true
      end

      local numQuestCurrencies = GetNumQuestLogRewardCurrencies(questID);
      for i = 1, numQuestCurrencies do
        local name, texture, numItems, currencyID = GetQuestLogRewardCurrencyInfo(i, questID);
        local text = BONUS_OBJECTIVE_REWARD_WITH_COUNT_FORMAT:format(texture, numItems, name);
        tinsert(rewardCurrency, {
          name = name,
          texture = texture,
          numItems = numItems,
          text = text
        })

        --dprint('  currency', i, currencyID, name, " |T"..tostring(texture)..":12:12|t")
        if shownCurrencies[currencyID] then
          rewardIcon = texture
          rewardCount = numItems
          rewardName = name
          rewardType = REWARD_CURRENCY
          foundPrimary = true
        end
      end

      local honorAmount = GetQuestLogRewardHonor(questID);
      if honorAmount > 0 then
        xpIcon = "Interface\\ICONS\\Achievement_LegionPVPTier4"
        xpCount = honorAmount
        xpName = HONOR
        xpType = HONOR_CURRENCY
        -- let items override
      end

      local numQuestRewards = GetNumQuestLogRewards(questID);
      if numQuestRewards > 0 then
        for i = 1, numQuestRewards do
          local name, texture, numItems, quality, isUsable, itemID = GetQuestLogRewardInfo(i, questID)

          if itemID then
            local itemName, itemLink, itemRarity, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc, itemTexture, sellPrice, classID, subclassID = GetItemInfo(itemID);
            if ( classID == LE_ITEM_CLASS_WEAPON or classID == LE_ITEM_CLASS_ARMOR or (classID == LE_ITEM_CLASS_GEM and subclassID == LE_ITEM_GEM_ARTIFACTRELIC) ) then
              rewardType = REWARD_GEAR
              rewardIcon = texture
              rewardName = name
              rewardCount = numItems
              foundPrimary = true
            elseif IsArtifactPowerItem(itemID) then
              rewardType = REWARD_ARTIFACT_POWER
              rewardIcon = texture
              rewardName = name
              rewardCount = self:UpdateArtifactPower(itemLink)
              foundPrimary = true
              --dprint('is an AP token')
            elseif classID == LE_ITEM_CLASS_TRADEGOODS then
              rewardType = REWARD_REAGENT
              rewardIcon = texture
              rewardName = name
              rewardCount = numItems
              foundPrimary = true
            end
            --dprint('  reward', i, name, " |T"..tostring(texture)..":12:12|t", quality, isUsable, itemID)
            tinsert(rewardItems, {
              name = name,
              texture = texture,
              numItems = numItems,
              quality = quality,
              isUsable = isUsable
            })
            if not foundPrimary then
              --print('  -updating primary')
              rewardType = REWARD_REAGENT

              rewardIcon = texture
              rewardName = name
              rewardCount = numItems
            end

          end

        end
      end

      --dprint('  '..self.questID..':|cFFFFFF00UpdateRewards():', numQuestRewards, rewardType)
      --dprint('  ', tostring(self.title), " |T"..tostring(self.itemTexture)..":12:12|t", tostring(self.itemName))

      if rewardType and ((self.itemNumber ~= rewardCount) or (self.rewardType ~= rewardType) or (self.itemName ~= rewardName) or (self.itemTexture ~= rewardIcon)) then
        if DATA_DEBUG and (self.debugTimer <= 0) then
          return false
        end

        return true, rewardType, rewardName, rewardIcon, rewardCount, quality, xpType, xpName, xpIcon, xpCount

      else
        return false
      end
    end
  end
end


function QuestPOI:UpdateArtifactPower(rewardLink)
  if not (rewardLink or self.rewardLink) then
    return
  end

  self.rewardLink = rewardLink or self.rewardLink

  local rewardCount
  WorldPlanTooltip:SetOwner(self, 'ANCHOR_NONE')
  WorldPlanTooltip:SetHyperlink(rewardLink or self.rewardLink)
  for i = 1, WorldPlanTooltip:NumLines() do
    local line = _G['WorldPlanTooltipTextLeft' .. i]
    local text = line and line:GetText()
    local multiplier = (text:match('million') and 1000000) or 1
    if text then
      text = text:gsub(',', '')
      local ap = text:match('([%d]+) '..ARTIFACT_POWER)
      if not ap then
        ap = text:match('([%d%.]+) million '..ARTIFACT_POWER)
        if ap then
          ap = tonumber(ap) * 1000000
        end
      end
      if ap then
        rewardCount = tonumber(ap)
      end
    end
  end
  return rewardCount
end

-- Applies position and sizing parameters to the pin data
function QuestPOI:SetAnchor(owner, dX, dY, mapWidth, mapHeight, scaleFactor)
  --dprint(self:GetName()..':SetAnchor()', owner, dX, dY, scaleFactor, self.filtered, self.used)
  if not self.used then
    self.hideReason = 'SetAnchor() on an unused frame.'
    self:HideOrShowFrames(false)
    return
  end

  if owner then
    self:SetParent(owner)
    self.Overlay:SetParent(owner)
    --self:SetFrameStrata('HIGH')
    self:SetFrameLevel(pinBaseIndex + self:GetID())
    --self.Overlay:SetFrameStrata('HIGH')
    self.Overlay:SetFrameLevel(overlayBaseIndex + self:GetID())
    self.isStale = true
  else
    owner = self:GetParent()
  end

  self:SetFrameStrata(db.PinStrata)
  self.Overlay:SetFrameStrata(db.PinStrata)


  if scaleFactor then
    print('scaleFactor')
    self:SetScale(scaleFactor)
    self.Overlay:SetScale(scaleFactor)
    self.scaleFactor = scaleFactor
  end

  mapWidth = mapWidth or owner:GetWidth()
  mapHeight = mapHeight or owner:GetHeight()
  dX = dX or self.x
  dY = dY or self.y
  --print(owner:GetName() or tostring(owner), self:GetName(), owner:GetScale(), scaleFactor)
  if (self.x ~= dX) or (self.y ~= dY) or scaleFactor then

    self.x = dX
    self.y = dY
    local pX = (dX * mapWidth) * (1 / scaleFactor)
    local pY = (-dY * mapHeight) * (1 / scaleFactor)
    --print('to', pX, pY, self:GetScale())
    self:ClearAllPoints()
    self:SetPoint('CENTER', owner, 'TOPLEFT', pX, pY)
  end

end

-- Non-hieriarchical display states, checked separately from used/filtered states
function QuestPOI:OnCriteria()
  self.checkCriteria = nil
  local isCriteria, isBounty, isSpellTarget

  print('|cFFFFFF00OnCriteria(' ..self:GetID() .. ')' )

  for index, bounty in pairs(db.Bounties) do
    if (not IsQuestComplete(bounty.questID)) and IsQuestCriteriaForBounty(self.questID, bounty.questID) then
      isCriteria = true
      if db.selectedBounty == bounty then
        isBounty = true
      end
      --dprint('|cFF00FF88Criteria:|r', self.questID, bounty.questID, isCriteria, isBounty)
    end
  end

  if (self.isBounty ~= isBounty) or (self.isCriteria ~= isCriteria) then
    --wqprint('|cFF00FF00criteria state changed')
    self.isBounty = isBounty
    self.isCriteria = isCriteria
    self.isSpellTarget = isSpellTarget
  end
end

function QuestPOI:OnCursor()
  self.checkCursor = nil
  self.isSpellTarget = IsQuestIDValidSpellTarget(self.questID)
end

local cvar_check = {
  [REWARD_CASH] = 'worldQuestFilterGold',
  [REWARD_ARTIFACT_POWER] = 'worldQuestFilterArtifactPower',
  [REWARD_CURRENCY] = 'worldQuestFilterOrderResources',
  [REWARD_REAGENT]= 'worldQuestFilterProfessionMaterials',
  [REWARD_GEAR] = 'worldQuestFilterEquipment',
}


function QuestPOI:OnFilters ()
  local qType = self.worldQuestType

  self.canShow = false
  self.checkFilters = nil

  if not TQ_IsActive(self.questID) then
    self.hideReason = 'Filter check ended because quest is inactive.'
    self.used = nil
    self:SetShown(false)
    return
  end

  if qType == LE_QUEST_TAG_TYPE_PROFESSION then
    if not(self.isKnownProfession or db.Config.ShowAllProfessionQuests) then
      self.hideReason = 'Failed profession check.'
      self.used = nil
      self:SetShown(false)
      return
    end
  end

  local filtered
  for filterKey, value in pairs(db.UsedFilters) do
    if self[filterKey] ~= value then
      if not self.filtered then
        print('|cFFFF4400filtering', filterKey, value, '~=', self[filterKey], self.title)
      end
      filtered = true
    end
  end
  if self.rewardType  and cvar_check[self.rewardType] then
    if not GetCVarBool(cvar_check[self.rewardType])  then
      filtered = true
    end
  end

  print('  '..self.questID..':|cFFFFFF00OnFilters()|r', filtered, self.title)
  if self.filtered ~= filtered then
    wqprint('|cFF00FF00filter changed')
    self.isStale = true
  end

  self.canShow = true
  self.filtered = filtered
end

function QuestPOI:Refresh (event)
  print('|cFF00FF88Refresh(|r'..self:GetID()..'|cFF00FF88)|r', event, self.title)

  if not self:IsShown() then
    print('queued for Refresh')
    self.isStale = true
    return nil
  elseif IsQuestComplete(self.questID) then
    self:Release()
    return nil
  end
  if self.checkCriteria then
    self:OnCriteria()
  end
  if self.checkFilters then
    self:OnFilters()
  end
  if self.checkCursor then
    self:OnCursor()
  end

  local style = DEFAULT_STYLE
  if self.filtered or db.IgnoreTimers[self.questID] then
    print('choose minimized')
    style = MINIMIZED_STYLE
  elseif self.dataLoaded then
    print('choose reward type')
    style = REWARD_TYPE_STYLES[self.rewardType]
  else
    print('choose default')
  end

  local currentWidth = style.iconWidth or DEFAULT_STYLE.iconWidth
  local borderWidth = style.borderWidth or DEFAULT_STYLE.borderWidth
  local highlightWidth = style.highlightWidth or DEFAULT_STYLE.highlightWidth
  local tagSize = style.TagSize or DEFAULT_STYLE.TagSize
  local hideIcon = style.hideIcon or DEFAULT_STYLE.hideIcon
  local showtime = style.showTime or DEFAULT_STYLE.showTime
  local borderColor = style.border or DEFAULT_STYLE.border
  local textColor = style.textColor or DEFAULT_STYLE.textColor
  local questID = self.questID
  local iconBorder = self.RewardBorder
  local trackingBorder = self.HighlightBorder
  local icon = self.icon
  local count = self.count
  local hideNumbers = style.hideNumber or DEFAULT_STYLE.hideNumber
  self.showTime = (style.showTime ~= nil) and style.showTime or DEFAULT_STYLE.showTime


  local tagIcon = self.tagIcon
  self.maxAlertLevel = style.maxAlertLevel or DEFAULT_STYLE.maxAlertLevel

  if self.dataLoaded then
    if self.isNew then
      print('new pin, has data, cue fade')
      self:StartFading()
      self.isNew = nil
    end
  else
    icon:SetAlpha(self.toAlpha or 0)
    iconBorder:SetAlpha(self.toAlpha or 0)
  end

  if self.itemName then
    if self.itemNumber and (self.itemNumber > 1) and (not hideNumbers) then
      local numberString = self.itemNumber
      if self.itemNumber >= 1000000 then
        numberString = (floor(self.itemNumber/100000)/10) .. 'M'
      elseif self.itemNumber >= 10000 then
        numberString = floor(self.itemNumber/1000) .. 'k'
      elseif self.itemNumber >= 1000 then
        local numeral = floor(self.itemNumber/1000)
        local decimal = mod(self.itemNumber, 1000)
        numberString = numeral
        if decimal > 100 then
          numberString = numberString .. '.' .. tostring(floor(decimal/100))
        end
        numberString = numberString .. 'k'
      end

      self.count:SetText(numberString)
      self.count:SetTextColor(unpack(textColor))
      self.count:Show()
    else
      self.count:SetText(nil)
      self.count:Hide()
    end
  else
    self.count:Hide()
  end

  if db.Config.ShowVerboseInfo then
    self.Description:SetText(self.title .. "\n" .. floor(self.x*100+.5) .. "," .. floor(self.y*100+.5))
  end


  icon:SetSize(currentWidth, currentWidth)
  icon:SetMask(style.iconMask or DEFAULT_STYLE.iconMask)
  if self.itemTexture then
    --iconBorder:SetTexture(WORLD_QUEST_BORDER)

    if hideIcon then
      icon:SetTexture(PENDING_ICON)
      icon:SetDesaturated(true)
      icon:SetVertexColor(unpack(borderColor))
    else
      icon:SetTexture(self.itemTexture)
      icon:SetDesaturated(false)
      icon:SetVertexColor(1, 1, 1)
    end
  else
    --
    --icon:SetTexture(PENDING_ICON)
    --icon:SetDesaturated(true)
    --icon:SetVertexColor(unpack(borderColor))
  end
  local borderMask = style.borderMask or DEFAULT_STYLE.borderMask
  local borderSize = currentWidth + (borderWidth * 2) + (self.isCriteria and 2 or 0)


  iconBorder:SetSize(borderSize, borderSize)

  iconBorder:SetMask(borderMask)
  iconBorder:SetTexture(PENDING_BORDER)

  iconBorder:SetDesaturated(true)

  local highlightSize = borderSize + (highlightWidth * 2)
  trackingBorder:SetSize(highlightSize, highlightSize)
  trackingBorder:SetMask(borderMask)
  trackingBorder:SetTexture(PENDING_BORDER)

  self:SetSize(borderSize, borderSize)
  self.IconBackdrop:SetSize(currentWidth, currentWidth)



  iconBorder:SetPoint('CENTER', (style.x or 0), (style.y or 0))
  trackingBorder:SetPoint('CENTER', (style.x or 0), (style.y or 0))

  self.tagIcon:SetShown((not self.filtered) and true or false)
  self.tagIcon:SetAtlas(self.tagAtlas)
  self.EliteBorder:SetShown(self.isElite and not self.filtered)


  if self.isBounty then
    print('is bounty')
    iconBorder:SetVertexColor(trackingBorder:GetVertexColor())
    trackingBorder:SetVertexColor(unpack(BORDER_SELECTED_BOUNTY))
  elseif self.isCriteria then
    print('is criteria of a bounty')
    iconBorder:SetVertexColor(trackingBorder:GetVertexColor())
    trackingBorder:SetVertexColor(unpack(BORDER_CRITERIA))
  else
    print('is nothing')
    iconBorder:SetVertexColor(unpack(borderColor))
    trackingBorder:SetVertexColor(0,0,0,.5)
  end

  self.timeLabel:SetShown(style.showTime)
  self:UpdateStatus()

  if SpellCanTargetQuest() then
    if self.isSpellTarget then
      icon:SetVertexColor(1,1,1)
      self:EnableMouse(false)
    else
      icon:SetVertexColor(0.5,0.5,0.5,1)
      self:EnableMouse(true)
    end
  else
    icon:SetVertexColor(1,1,1)
    self:EnableMouse(true)
  end

  self.isStale = nil

  -- signal filter info update
  WorldPlanSummary.isStale = true
  return true
end

-- Called at static intervals and with Refresh
function QuestPOI:UpdateStatus()
  -- update time elements
  self.isActive = TQ_IsActive(self.questID)
  --print(self.maxAlertLevel)
  local border = (self.isBounty or self.isCriteria) and self.RewardBorder or self.HighlightBorder

  if self.isActive then

    local tl = self.alertLevel
    local timeLeft = TQ_GetQuestTimeLeftMinutes(self.questID)
    if timeLeft > 0 then

      local text, timeState, style = self:GetTimeInfo(timeLeft, self.maxAlertLevel)
      if tl ~= timeState then
        tl = timeState
        self.timeLabel:SetText(text)
      end
    end

    self.alertLevel = tl
    self.timeLabel:SetShown(self.worldQuest and (self.maxAlertLevel >= 1) and  self.showTime)
  else
    self.hideReason = "No longer active."
    self:HideOrShowFrames(false)

  end
end

-- Show/Hide the text overlays associated with the quest pin; they aren't hierarchically linked
function QuestPOI:HideOrShowFrames(isShown)
  if not isShown then
    -- print('|cFFFFFF00' ..self:GetName()..':HideOrShowFrames()')
    -- do not SetShown() here
    if not self.hideReason then
      self.hideReason = "HideOrShowFrames() called without a reason."
    end
  end
  self.Overlay:SetShown(isShown)
  self.count:SetShown(isShown)
  self.timeLabel:SetShown(isShown)
end

function QuestPOI:Release(msg)
  print('|cFFFF4400Release('..self:GetID()..')', self.hideReason)
  self.hideReason = msg or 'Released by script.'
  self:ClearAllPoints()
  self:SetShown(false)
  self:HideOrShowFrames(false)

  if self.questID then
    db.QuestsByID[self.questID] = nil
    for _, map in pairs(db.QuestsByZone) do
      map[self.questID] = nil
    end
    self.questID = nil
  end
  self.isActive = nil
  self.complete = nil
  self.used = nil
  self.dataLoaded = nil
  self.rewardType = nil
  self.itemTexture = nil
  self.itemName = nil
  self.itemNumber = nil
  self.animating = nil
  self.icon:SetAlpha(0)
  self.RewardBorder:SetAlpha(0)
  self:UnregisterEvent('QUEST_TURNED_IN')
  self:UnregisterEvent('QUEST_LOG_UPDATE')

  self.checkCursor = true
  self.checkFilters = true
  self.checkCriteria = true

  for i, pin in ipairs(db.UsedPins) do
    if pin == self then
      print('|cFFFF4400cleared from UsedPins|r')
      tremove(db.UsedPins, i)
      break
    end
  end

  tinsert(db.FreePins, self)
  WorldPlan.dataFlush = true
  WorldPlanSummary.isStale = true

  return true
end