view Modules/ArtifactPower.lua @ 123:b3c0258b419d v7.3.0-1

ArtifactPower: - XP progress bar fixes Currency: - More argus stuff - Block dimensions are calculated more consistently TalkingHead - Aesthetic changes - Default right-click behaviour restored (hides the text box)
author Nenue
date Tue, 05 Sep 2017 02:56:33 -0400
parents ea2c616a3b4f
children 3f4794dca91b
line wrap: on
line source
-- Veneer
-- ArtifactPower.lua
-- Created: 1/15/2017 11:44 PM
-- %file-revision%
--

local print = DEVIAN_WORKSPACE and function(...) print('VnAP', ...) end or nop
VeneerArtifactPowerMixin = {
  numItems = 0,
  Tokens = {},
  cache = {},
  fishingCache = {},
  scanQueue = {},
  worldQuestAP = 0,
  worldQuestItems = {},
  ItemButtons = {},
  anchorGroup = 'TOP',
  anchorPoint = 'TOPLEFT',
  anchorPriority = 3,
  anchorFrom = 'BOTTOMLEFT',
  moduleName = 'Artifactor',
  HideCombat = true
}

VeneerArtifactButtonMixin = {}
local Artifact = VeneerArtifactButtonMixin

local defaultSettings = {
  firstUse = true,
  autoHide = true,
}
local Module = VeneerArtifactPowerMixin
local BAGS_TO_SCAN = {BACKPACK_CONTAINER}
local TOOLTIP_NAME = 'VeneerAPScanner'
local XP_INSET = 1
local XP_WIDTH = 4
local FISHING_MAX_TRAITS = 24
local WEAPON_MAX_TRAITS = 92
local FRAME_PADDING = 4
local EQUIPPED_SIZE = 64
local BUTTON_SIZE = 48
local FRAME_LIST = {'ContainerFrame1', 'BankFrame'}
local BAG_FRAMES = {'ContainerFrame1'}
local BANK_FRAMES = {'BankFrame'}

function Module:OnLoad()
  self:RegisterEvent('BAG_UPDATE') -- use to obtain bag IDs to scan
  self:RegisterEvent('BAG_UPDATE_DELAYED') -- use to trigger actual scan activity
  self:RegisterEvent('BANKFRAME_OPENED')  -- bank info available
  self:RegisterEvent('BANKFRAME_CLOSED')  --
  self:RegisterEvent('ARTIFACT_UPDATE')    -- visible data change
  self:RegisterEvent('ARTIFACT_XP_UPDATE') -- xp for equipped artifact
  self:RegisterEvent('PLAYER_REGEN_ENABLED') -- combat
  self:RegisterEvent('PLAYER_REGEN_DISABLED') --
  self:RegisterEvent('PLAYER_ENTERING_WORLD') -- zone/instance transfer
  self:RegisterEvent('ITEM_LOCK_CHANGED') -- use to clear bag slot cache data
  Veneer:AddHandler(self)
  SLASH_VENEER_AP1 = "/vap"
  SLASH_VENEER_AP2 = "/veneerap"
  SlashCmdList.VENEER_AP = function(arg)
    if arg == 'fishing' then
      if VeneerData.ArtifactPower.EnableFishing then
        VeneerData.ArtifactPower.EnableFishing = nil
      else
        VeneerData.ArtifactPower.EnableFishing = true
      end
      self:Print('Show Underlight Angler:', (VeneerData.ArtifactPower.EnableFishing and 'ON' or 'OFF'))
      self:Update()
    elseif arg == 'reset' then
      if self.db then
        table.wipe(self.db.cache)
        table.wipe(self.db.fishingCache)
      end
      self:Print('Cache data reset.')
      self:Update()
    elseif arg:match('item') then
      print('name', arg:match("^item (%S.+)"))
      print('num', arg:match("Hitem:(%d+)"))
      local linkOrID = arg:match("item:(%d+)") or arg:match("^item (%S+)")
      if linkOrID then
        local name, etc = GetItemInfo(linkOrID)
      end

    else

      self:Show()
    end
  end

  self.tooltip = CreateFrame('GameTooltip', TOOLTIP_NAME, self, 'GameTooltipTemplate')


end
local ShortNumberString = function (value)
  if value >= 1000000000 then

    return tostring(floor(value/10000000)/100) .. 'B'
  elseif value >= 1000000 then
    return tostring(floor(value/100000)/10) .. 'M'
  elseif value >= 100000 then
    return tostring(floor(value/1000)) .. 'k'
  elseif value >= 1000 then
    return tostring(floor(value/100)/10) .. 'k'
  else
    return value
  end
end


local IsBagnonOpen = function()
  return ((BagnonFramebank and BagnonFramebank:IsShown()) or (BagnonFrameinventory and BagnonFrameinventory:IsShown()))
end
local addonCompatibility = {
  ['Bagnon'] = {
    BagFrames = {'BagnonFrameinventory'},
    BankFrames = {'BagnonFramebank'},
    FrameMethods = {
      ['Hide'] = IsBagnonOpen,
      ['Show'] = IsBagnonOpen
    },
    PostHooks = {},
    MethodClass = 'Bagnon',
    MethodHooks = {'BANK_OPENED', 'BANKFRAME_CLOSED'},

  }
}



local queued_hooks = {}
local function CreateHook(...)
  if select('#', ...) >= 2 then
    tinsert(queued_hooks, {...})
  end
  if not InCombatLockdown() then
    local info = tremove(queued_hooks)
    while info do
      --[[local oFunc = tremove(info, #info)
      local args = info

      local func = function(...)
        print('|cFFFF0088Callback:|r', unpack(args))

        oFunc(...)
      end
      print('hooking', unpack(info), oFunc, func)
      hooksecurefunc(unpack(info), func)
      --]]
      hooksecurefunc(unpack(info))
      info = tremove(queued_hooks)
    end

  end
end

local function AddFrameHooks(frame, args)
  for funcName, func in pairs(args.FrameMethods) do
    print('binding', frame:GetName(), funcName, 'to', tostring(func))
    CreateHook(frame, funcName, function()
      print(frame:GetName(), funcName, 'hook')
      VeneerArtifactPower:TryToShow()
    end)
  end
end
local PENDING_HOOKS = {}
local guid = UnitGUID('player')

local function RegisterInventoryFrame(name, listType, args)
  print('register', name, 'as inventory frame type =', (listType == BAG_FRAMES) and 'bags' or 'bank')
  tinsert(FRAME_LIST, name)
  tinsert(listType, name)
  if _G[name] then
    AddFrameHooks(_G[name], args)
  else
    PENDING_HOOKS[name] = args
  end
end

function Module:Setup()
  print(self:GetName()..':Setup()')
  guid = UnitGUID('player')
  VeneerData.ArtifactPower = VeneerData.ArtifactPower or defaultSettings
  self.db = VeneerData.ArtifactPower
  self.db[guid] = self.db[guid] or {}
  self.db.cache = self.db.cache or {}
  self.db.fishingCache = self.db.fishingCache or {}

  for i, data in pairs(self.cache) do
    -- bring in anything found before player data is active
    self.db.cache[i] = data
  end
  for i, data in pairs(self.fishingCache) do
    self.db.fishingCache[i] = data
  end

  self.profile = self.db[guid]
  self.profile.cache = self.profile.cache or {}
  self.profile.cache.bagItems = self.profile.cache.bagItems or {}
  self.profile.cache.bags = self.profile.cache.bags or {}
  self.profile.cache.fishing = self.profile.cache.fishing or {}
  self.profile.cache.items = self.profile.cache.items or {}
  self.profile.bagslots = self.profile.bagslots or {}
  self.profile.artifacts = self.profile.artifacts or {}
  self.updateSummary = true
  self.cache = self.profile.cache

  VeneerArtifactPowerTimer:SetScript('OnUpdate', function()
    self:OnUpdate()
  end)

  local DoTryToShow = function()

    self:TryToShow()
  end
  CreateHook("OpenBackpack", DoTryToShow)
  CreateHook("CloseBackpack", DoTryToShow)

  -- Bagnon compatibility
  -- todo: ArkInventory, Elv, etc
  for addon, args in pairs(addonCompatibility) do
    if IsAddOnLoaded(addon) then

      for _, name in ipairs(args.BagFrames) do
        RegisterInventoryFrame(name, BAG_FRAMES, args)
      end
      for _, name in ipairs(args.BankFrames) do
        RegisterInventoryFrame(name, BANK_FRAMES, args)
      end

      -- should only specify non-secure functions in this table
      for _, name in ipairs(args.PostHooks) do
        local oFunc = _G[name]
        print('hook entry', name, tostring(oFunc))
        CreateHook(name, function(...)
          print('|cFFFF0088' .. name .. '|r', ..., 'original', tostring(oFunc))
          oFunc(...)
          self:TryToShow()
        end)
      end
      local frame = _G[args.MethodClass]
      if frame then
        print()
        for _, name in ipairs(args.MethodHooks) do
          CreateHook(frame, name, DoTryToShow)
        end
      end
    end
  end

  if self.db.firstUse then
    self.db.firstUse = nil

  end
end

local UNDERLIGHT_ANGLER_ID = 133755

function Module:ResetCache()
  table.wipe(self.cache.items)
  table.wipe(self.cache.fishing)
  table.wipe(self.cache.bags)
  table.wipe(self.cache.bagItems)
  self:ScanAllBags()
end

function Module:QueueBag(containerID)
  containerID = tonumber(containerID)
  if not containerID then
    return
  end

  if not tContains(BAGS_TO_SCAN, containerID) then
    print(' queueing', containerID, type(containerID), #BAGS_TO_SCAN , 'in line')
    BAGS_TO_SCAN[#BAGS_TO_SCAN + 1] = containerID
  end
end

function Module:Reanchor()
  if Veneer then
    Veneer:DynamicReanchor()
  end
end

function Module:OnShow()
  print('|cFFFFFF00OnShow()|r')

  for name, args in pairs(PENDING_HOOKS) do
    if _G[name] then
      AddFrameHooks(_G[name], args)
      PENDING_HOOKS[name] = nil
    end
  end

  self:UpdateWorldQuestsAP()
  self:RegisterEvent('QUEST_LOG_UPDATE')
  self.enabled = true

  self:ScanAllBags()
  self:Reanchor()
end
function Module:OnHide()
  print('|cFF88FF00OnHide()|r', debugstack())
  self:UnregisterEvent('QUEST_LOG_UPDATE')
  self:Reanchor()
end
function Module:OnEnter()

  GameTooltip:SetOwner(self, 'ANCHOR_CURSOR')


  GameTooltip:AddLine(self.bagAP)
  GameTooltip:AddLine(self.bankAP)

end

function Module:TryToShow()

  print('|cFFFFFF00TryToShow()')

  if not InCombatLockdown() then
    for _, name in ipairs(FRAME_LIST) do
      print('test:', name, (_G[name] and _G[name]:IsShown()))
      if _G[name] and _G[name]:IsVisible() then
        if self:IsShown() then
          self:Update()
        else
          self:Show()
        end
        return
      end
    end
  end

  print('failed tests')
  self:Hide()
end


function Module:OnEvent(event, ...)
  print('|cFF00FF88OnEvent()', event, ...)
  if event == 'PLAYER_ENTERING_WORLD' then
    self:TryToShow()
  elseif event == 'BAG_UPDATE' then
    local containerID = ...


    self:QueueBag(containerID)
  elseif event == 'ITEM_LOCK_CHANGED' then

    local containerID, slotID = ...

    if self.cache.bags[containerID] and self.cache.bags[containerID][slotID] then
      self.cache.bags[containerID][slotID] = nil
      self.cache.fishing[containerID][slotID] = nil
    end


  elseif event == 'PLAYER_BANKSLOTS_CHANGED' then
    self:ScanAllBags()
  elseif event == 'BAG_UPDATE_DELAYED' then
    if not self.firstHit then
      -- prevent double call from login
      self.firstHit = true
    else
      self:ScanAllBags()
    end
  elseif event == 'BANKFRAME_OPENED' then
    self.bankAccess = true
    self:ScanAllBags()
  elseif event == 'BANKFRAME_CLOSED' then
    self.bankAccess = nil
  elseif event == 'ARTIFACT_UPDATE' then
    local newItem = ...
    if newItem then
      local itemID, _, name, texture, currentXP, pointsSpent = C_ArtifactUI:GetArtifactInfo()
      self:UpdateArtifact(itemID, name, texture, currentXP, pointsSpent)
      --self:ScanAllBags(self.bankAccess)
    end
  elseif event == 'ARTIFACT_XP_UPDATE' then
    local itemID, _, name, texture, currentXP, pointsSpent = C_ArtifactUI:GetEquippedArtifactInfo()
    self:UpdateArtifact(itemID, name, texture, currentXP, pointsSpent)
    --self:ScanAllBags(self.bankAccess)
  elseif event == 'PLAYER_REGEN_ENABLED' then

    if self.queuedScan then
      self:ScanAllBags(self.backAccess)
    else
      self:TryToShow()
    end

    if #queued_hooks >= 1 then
      CreateHook()
    end
  elseif event == 'QUEST_LOG_UPDATE' then
    self:UpdateWorldQuestsAP()
  elseif event == 'PLAYER_REGEN_DISABLED' then
    self:Hide()
  end
end

function Module:OnUpdate()
  if #self.scanQueue >= 1 then
    local scanInfo = tremove(self.scanQueue, 1)
  end
  if IsShiftKeyDown() then
    self.Refresh:Show()
  else
    self.Refresh:Hide()
  end

end

function Module:OnMouseDown(button)
  self.enabled = nil
  if button == 'RightButton' then
    self:Hide()
  end

end

function Module:Update()
  if not self:IsShown() then
    print('|cFFFF4400Update()|r')
    return
  end
  print('|cFFFFFF00pdate()|r')

  local numButtons = 0
  local contentsHeight = 16
  local contentsWidth = 400
  if self.profile.knowledgeMultiplier then
    local artifactsWidth = self:UpdateArtifactButtons()

    if artifactsWidth ~= 0 then
      contentsHeight = contentsHeight + 64
    end

    contentsWidth = max(contentsWidth, min(artifactsWidth, 400))

    local itemsWidth, itemsHeight = self:UpdateItemButtons()
    contentsHeight = contentsHeight + itemsHeight
    contentsWidth = max(contentsWidth, itemsWidth)
  end



  local bankText, bagText
  if not self.profile.knowledgeMultiplier then
    bankText = '|cFF00FF00Shift-Right-Click an artifact weapon to start building data.'
  elseif not (self.bankAP and self.bagAP) then
    bankText = '|cFFFF0000Open bank frame to count all AP|r '
  else

    if self.bagAP and (self.bagAP > 0) then
      bankText = 'Inventory: |cFFFFFFFF' .. ShortNumberString(self.bagAP) .. '|r'
    end
    if self.bankAP and (self.bankAP > 0) then
      bankText = (bankText and (bankText .. ' | ') or '') .. '|cFFFFFF00'..ShortNumberString(self.bankAP)..' banked|r'
    end
    if self.fishingAP and self.fishingAP > 0 then
      bankText = (bankText and (bankText .. ' | ') or '') .. '|cFF0088FF' .. ShortNumberString(self.fishingAP) .. ' fishing|r'
    end
  end

  if self.worldQuestAP then
    bankText = (bankText and (bankText .. '\n') or '') .. '|cFFFFBB00World Quests:|r |cFFFFFFFF' .. ShortNumberString(self.worldQuestAP) .. ''
  end


  self.SummaryHeader:SetText(bankText)
  if not self.lastButton then
    contentsHeight = contentsHeight + self.SummaryHeader:GetHeight()
  end


  if not self.hasArtifacts then
    self:SetShown(false)
  end


  self:SetWidth(contentsWidth)
  self:SetHeight(contentsHeight)
  self:Reanchor()
end

local BROKEN_ISLE_ID = 1007

function Module:UpdateWorldQuestsAP()
  self.waitingForQuestRewardData = false
  self.worldQuestAP = 0
  wipe(self.worldQuestItems)

  for zoneIndex = 1, C_MapCanvas.GetNumZones(BROKEN_ISLE_ID) do
    local zoneMapID, zoneName, zoneDepth, left, right, top, bottom = C_MapCanvas.GetZoneInfo(BROKEN_ISLE_ID, zoneIndex);
    --print(zoneMapID, zoneName)
    if zoneDepth <= 1 then -- Exclude subzones
      local taskInfo = C_TaskQuest.GetQuestsForPlayerByMapID(zoneMapID, BROKEN_ISLE_ID);

      if taskInfo then
        for i, info in ipairs(taskInfo) do
          local questID = info.questId

          local questTitle, factionID, capped = C_TaskQuest.GetQuestInfoByQuestID(questID)
          --print(questTitle, HaveQuestRewardData(questID))
          if HaveQuestRewardData(questID) then


            local numQuestRewards = GetNumQuestLogRewards(questID);

            if numQuestRewards > 0 then
              for i = 1, numQuestRewards do
                local name, texture, numItems, quality, isUsable, itemID = GetQuestLogRewardInfo(i, questID)
                if IsArtifactPowerItem(itemID) then
                  local _, link = GetItemInfo(itemID)
                  if link then
                    local ap = self:GetItemAP(itemID, link)
                    --print('ap =', ap)
                    if ap then
                      self.worldQuestAP = self.worldQuestAP + ap

                    end

                    self.worldQuestItems[itemID] = (self.worldQuestItems[itemID] or 0) + 1
                  end

                  --print(self.worldQuestAP)
                end

              end

            end


          else
            C_TaskQuest.RequestPreloadRewardData(questID);
            self.waitingForQuestRewardData = true
          end
        end
      end
    end
  end
end

function Module:UpdateArtifactButtons()

  -- Artifact icons, in no particular order
  self.equippedID = C_ArtifactUI.GetEquippedArtifactInfo()
  self.lastButton = nil
  self.canAddAP = nil
  self.canAddFishingAP = nil
  local hasArtifacts
  local numButtons = 0
  local lastFrame = self
  local fishingID, fishingData
  local index, button
  local equipped =self.profile.artifacts[self.equippedID]
  local buttonsWidth = 0
  if equipped then
    numButtons = numButtons + 1
    button = self.Artifact[numButtons]
    button.relativeFrame = self
    if self.equippedID ~= button.itemID then
      button:SetItem(self.equippedID, equipped, numButtons, true, nil)
      hasArtifacts = true
    end
    lastFrame = button
    buttonsWidth = EQUIPPED_SIZE + (FRAME_PADDING * 2)
  end
  for itemID, artifact in pairs(self.profile.artifacts) do
    if (itemID == UNDERLIGHT_ANGLER_ID) then
      -- only add if we have fishing AP items and it's not being shown in the equipped slot
      if VeneerData.ArtifactPower.EnableFishing and (itemID ~= self.equippedID) then
        fishingID = itemID
        fishingData = artifact
      end

      if artifact.level < FISHING_MAX_TRAITS then
        if itemID == self.equippedID then
          self.canAddFishingAP = true
        end
      end
    else
      if artifact.level < WEAPON_MAX_TRAITS then
        if itemID == self.equippedID then
          self.canAddAP = true
        else

          hasArtifacts = true
          numButtons = numButtons + 1
          button = self.Artifact[numButtons]
          button.relativeFrame = lastFrame
          if button.itemID ~= itemID then
            button:SetItem(itemID, artifact, numButtons, (self.equippedID == itemID), nil)
          end
          lastFrame = button
          buttonsWidth = buttonsWidth + lastFrame:GetWidth() + FRAME_PADDING
        end
      end
    end
  end

  self.lastButton = lastFrame


  if fishingData and (self.fishingAP and self.fishingAP > 0) then
    numButtons = numButtons + 1
    hasArtifacts = true
    local button = self.Artifact[numButtons]
    button.relativeFrame = lastFrame
    button.isFishing = true
    button:SetItem(fishingID, fishingData, numButtons, self.equippedID == fishingID)
    self.lastButton = button
  end

  self.hasArtifacts = hasArtifacts
  for i = numButtons+ 1, #self.Artifact do
    print('hide', i)
    self.Artifact[i]:Hide()
  end

  self.SummaryHeader:ClearAllPoints()
  if self.lastButton then
    self.SummaryHeader:SetPoint('TOPLEFT', self.lastButton, 'TOPRIGHT', 4, -2)
  else
    self.SummaryHeader:SetPoint('BOTTOMLEFT', self, 'BOTTOMLEFT', 4, 4)
  end



  return buttonsWidth
end


function Module:UpdateItemButtons()
  print('|cFF00FFFFUpdateItemButtons()|r')

  local apType
  if self.canAddFishingAP then
    apType = true
  elseif not self.canAddAP then
    for index, button in ipairs(self.Tokens) do
      button:Hide()
    end
    return 0, 0
  end


  local lastFrame, upFrame
  local numButtons = 0
  local buttonsHeight = 0
  local buttonsWidth = 0

  for index, button in ipairs(self.Tokens) do
    if (button.numItems >= 1) and (button.isFishingAP == apType) then
      if button.itemName then
        self:SetItemAction(button)
      end

      button:ClearAllPoints()
      numButtons = numButtons + 1
      local col = mod(numButtons,8)
      print(index, button:GetID(), button.Icon:GetTexture())
      if numButtons == 1 then
        button:SetPoint('TOPLEFT', self, 'TOPLEFT', 4, -76)
        upFrame = button
        buttonsHeight = 52
      else
        if col == 1 then
          button:SetPoint('TOPLEFT', upFrame, 'BOTTOMLEFT', 0, -2)
          upFrame = button
          buttonsHeight = buttonsHeight + 52

        else
          button:SetPoint('TOPLEFT', lastFrame, 'TOPRIGHT', 2, 0)

        end
      end

      button.Count:SetText(button.numItems)
      lastFrame = button
      button:Show()
    else
      button:Hide()
    end
    buttonsWidth = min(numButtons, 8) * (BUTTON_SIZE)
  end



  if buttonsWidth ~= 0 then
    buttonsWidth = buttonsWidth + 8+ ((min(numButtons, 8)-1)*2)
  end



  return buttonsWidth, buttonsHeight
end

function Module:SetItemAction(button, name)
  name = name or self.itemName
  if InCombatLockdown() then
    self.itemName = name
    return
  else
      button:SetAttribute('*type*','item')
    button:SetAttribute('*item*', name)
  end
end

function Module:GetItemButton(itemID, texture, itemAP, fishing)
  print('|cFF00FFFFGetItemButton()|r', itemID, texture, itemAP)
  local button = self.ItemButtons[itemID]

  if not button then
    button = CreateFrame('Button', 'VeneerAPToken'..itemID, self, 'VeneerItemButton')
    button.baseAP = itemAP

    button:SetPushedTexture([[Interface\Buttons\UI-Quickslot-Depress]])
    button:SetHighlightTexture([[Interface\Buttons\ButtonHilight-Square]],"ADD")
    button:SetID(itemID)
    button.numItems = 0
    button.Icon:SetTexture(texture)
    button:RegisterForClicks("AnyUp")
    button.isFishingAP = fishing
    self:SetItemAction(button, GetItemInfo(itemID))

    print('  created')
    self.ItemButtons[itemID] = button
    self.numItems = self.numItems +  1
  end

  button.Label:SetText(ShortNumberString(itemAP))

  button.numItems = button.numItems + 1
  return button
end

function Module:GetItemAP(itemID, itemLink, bagData)
  if not self.cache.items[itemID] then

    print('doing tooltip scan', itemLink, itemID)
    self.tooltip:SetOwner(self, 'ANCHOR_NONE')
    self.tooltip:SetHyperlink(itemLink)
    self.tooltip:Show()
    local numLines = self.tooltip:NumLines()
    if numLines >= 3 then
        for i = 3, numLines do
          local text = _G[TOOLTIP_NAME .. 'TextLeft'.. i]:GetText()
          if text then

            text = text:lower():gsub(',', '')

            if text:match('equipped artifact') then
              print(itemLink, '-', tonumber(text))

              local itemAP = text:match('[%d%.]+')
              if itemAP then
                -- tokens > 1M are described as '%f million'
                if text:match("million") then
                  itemAP = tonumber(itemAP) * 1000000
                end

                itemAP = itemAP
                self.cache.items[itemID] = tonumber(itemAP)
              end
            end
            if text:match('fishing artifact') then
              local fishingAP = text:match("%d+")
              fishingAP = fishingAP
              print(itemLink, 'fishing', tonumber(text))
              if fishingAP then
                self.cache.items[itemID] = tonumber(fishingAP)
                self.cache.fishing[itemID] = true
              end
            end
          end
        end
    else

      self.cache.items[itemID] = 0
    end
  end
  return self.cache.items[itemID], self.cache.fishing[itemID]
end

function Module:UpdateArtifact(itemID, name, texture, currentXP, pointsSpent)
  print('|cFF00FF00UpdateArtifact()|r')
  if not self.profile then
    return
  end
  local artifacts = self.profile.artifacts

  if itemID then
    self.currentEquipped = itemID

    artifacts[itemID] = artifacts[itemID] or {}
    table.wipe(artifacts[itemID])
    local artifact = artifacts[itemID]

    artifact.name = name
    artifact.texture = texture
    artifact.currentXP = currentXP
    artifact.level = pointsSpent
    artifact.tier = C_ArtifactUI.GetArtifactTier() or ((pointsSpent >= 36) and 2 or 1)
    artifact.itemID = itemID

    print('tier', artifact.tier)
    local cost = C_ArtifactUI.GetCostForPointAtRank(pointsSpent, artifact.tier)
    artifact.currentCost = cost

    for index, frame in pairs(self.Artifact) do
      if frame.itemID == itemID then
        frame:SetItem(itemID, artifact, index, (itemID == self.equippedID), (itemID == UNDERLIGHT_ANGLER_ID))
      end
    end

  end
end

function Module:ScanBag(id)
  print('|cFF00FFFFScanBag()|r', id, IsBagOpen(id), GetContainerNumSlots(id))
  local numSlots = GetContainerNumSlots(id)
  local requiresUpdate
  if numSlots == 0 then
    return nil
  end


  self.profile.bagslots[id] = self.profile.bagslots[id] or {}
  table.wipe(self.profile.bagslots[id])
  local bagData = self.profile.bagslots[id]
  bagData.totalAP = 0
  bagData.fishingAP = 0
  bagData.items = bagData.items or {}
  table.wipe(bagData.items)
  local c = self.cache

  c.bagItems[id] = c.bagItems[id] or {}
  c.bags[id] = c.bags[id] or {}
  c.fishing[id] = c.fishing[id] or {}

  for slotID = 1, numSlots do
    local texture, count, locked, quality, readable, lootable, link = GetContainerItemInfo(id, slotID)
    if link then
      local itemID = GetContainerItemID(id, slotID)
      local name, _, quality, iLevel, reqLevel, class, subclass = GetItemInfo(link)

      if class == 'Consumable' or subclass == 'Cooking' then
        --print(GetItemInfo(link))
        local itemAP, isFishingAP
        if c.bags[id][slotID] and (c.bagItems[id][slotID] == itemID) then
          --print('cached slot', id, slotID, name)
          itemAP = c.bags[id][slotID]
          isFishingAP = c.fishing[id] and c.fishing[id][slotID]
        else
          itemAP, isFishingAP = self:GetItemAP(itemID, link)
          c.bagItems[id][slotID] = itemID
          c.bags[id][slotID] = itemAP
          c.fishing[id][slotID] = isFishingAP
        end


        --print(itemAP, isFishingAP)
        if itemAP and (itemAP > 0) then
          local itemButton = self:GetItemButton(itemID, texture, itemAP, isFishingAP)

          if isFishingAP then
            bagData.fishingItems = (bagData.fishingItems or 0) + 1
            bagData.fishingAP = (bagData.fishingAP or 0) + itemAP
          else
            itemAP = itemAP
            bagData.numItems = (bagData.numItems or 0) + 1
            bagData.totalAP = (bagData.totalAP or 0) + itemAP
          end
          bagData.items[itemID] = (bagData.items[itemID] or 0) + 1
        end
      elseif self.profile.artifacts[itemID] then
        --print('artifact weapon', itemID, link, id, slotID)
        self.profile.artifacts[itemID].containerID = id
        self.profile.artifacts[itemID].slotID = slotID
      else
        --print('skipping', class, subclass, link)
      end

    end

  end

end

local BAG_SLOTS = {0, 1, 2, 3, 4 }
local BANK_SLOTS = {-1, 5, 6,7, 8, 9, 10, 11, 12}
local ItemCounts = {}
function Module:ScanAllBags()
  if InCombatLockdown() then
    self.queuedScan = true
    return
  end
  if not self.profile.knowledgeMultiplier then
    print('need to get knowledge level')
    return
  end

  self.queuedScan = nil

  print('|cFFFF0088ScanAllBags()|r', self.profile.knowledgeMultiplier)

  for _, button in ipairs(self.Tokens) do
    button.numItems = 0
  end


  for _, bagID in ipairs(BAG_SLOTS) do
    self:ScanBag(bagID)
  end

  if self.bankAccess then
    for _, bagID in ipairs(BANK_SLOTS) do
      self:ScanBag(bagID)
    end
  end

  self.bankAP = 0
  self.bagAP = 0
  self.fishingAP = 0

  table.wipe(ItemCounts)
  for id, bagData in pairs(self.profile.bagslots) do
    print(id, GetBagName(id), bagData.totalAP, bagData.fishingAP)
    id = tonumber(id)
    if bagData.totalAP then
      if (id == BANK_CONTAINER) or (id >= 5) then
        self.bankAP = self.bankAP + bagData.totalAP
      else
        self.bagAP = self.bagAP + bagData.totalAP
      end
    end
    if bagData.fishingAP then
      self.fishingAP = self.fishingAP + bagData.fishingAP
    end

  end
  self.lastUpdate = GetTime()
  self.queuedScan = nil
  self:TryToShow()
end


function Artifact:SetItem(itemID, artifact, index, equipped, fishing)
  print('|cFF00FFFFSetItem()|r', itemID, index)
  print(artifact.name, artifact.texture, artifact.currentXP)

  if not artifact.currentCost then
    artifact.currentCost = artifact.cost
  end

  for k,v in pairs(artifact) do
    --print('::',k,v)
    self[k] = v
  end

  self.isFishing = fishing
  -- this can change between artifact parses
  local unusedXP = (itemID ~= UNDERLIGHT_ANGLER_ID) and ((self:GetParent().bankAP or 0) + (self:GetParent().bagAP or 0)) or (self:GetParent().fishingAP or 0)
  print('unspent:', unusedXP)

  -- current    standing artifact XP (what appears in the artifact ui)
  -- actual     artifact XP after any unlocked points are spent
  -- total      total of invested and inventory XP
  -- totalCost  total of costs between current and actual level
  local actualXP = artifact.currentXP
  local actualLevel = artifact.level
  local actualCost = C_ArtifactUI.GetCostForPointAtRank(actualLevel, artifact.tier)

  print('tier:', artifact.tier)
  print('current:', self.level, self.currentXP, '/', self.currentCost)
  while actualXP >= actualCost do
    actualXP = actualXP - actualCost
    actualLevel = actualLevel + 1
    actualCost = C_ArtifactUI.GetCostForPointAtRank(actualLevel, artifact.tier)

    print('* ', actualLevel, actualXP, actualCost, totalCost)
  end
  print('actual:', actualLevel, actualXP, '/', actualCost)


  local totalXP = actualXP + unusedXP
  local totalCost = actualCost
  local totalLevel = actualLevel

  local remaining = totalXP
  local nextCost = artifact.currentCost
  print(totalXP, totalCost)
  if remaining > nextCost then
    while remaining >= nextCost do
      totalLevel = totalLevel + 1
      remaining = remaining - nextCost
      nextCost = C_ArtifactUI.GetCostForPointAtRank(totalLevel, artifact.tier)
      print('|cFFFFFF00+ ', totalLevel, remaining, '/', totalCost)
    end
    totalXP = remaining
    totalCost = nextCost
  end
  print('total:', totalLevel, totalXP, '/', totalCost)

  self.currentLevel = self.level


  self.actualCost = actualCost
  self.actualLevel = actualLevel
  self.actualXP = actualXP

  self.totalXP = totalXP
  self.totalCost = totalCost
  self.totalLevel = totalLevel



  if index ~= 1 then
    self:ClearAllPoints()
    self:SetPoint('TOPLEFT', self.relativeFrame, 'TOPRIGHT', 4, 0)
  else
    self:ClearAllPoints()
    self:SetPoint('TOPLEFT', self.relativeFrame, 'TOPLEFT', 4, -4)
  end

  self.itemID = itemID
  self.isEquipped = equipped
  self:Update()
  self:Show()

  return self
end

function Artifact:UpdateXPBar()
  local r3, g3, b3 = 1, .5, 0 -- main xp bar
  -- current:   amount shown in blizz ui
  -- actual:    amount contributing the next level, will be same until current point cap is reached
  -- potential: total of ap on hand
  print(self.currentXP, self.actualXP, self.potentialXP)

  local maxHeight = self:GetHeight() - (XP_INSET*2)
  local currentHeight = self.CurrentProgress:GetHeight() or 1
  local offHeight = self.AdjustedProgress:GetHeight() or 1


  self.CurrentProgress:SetPoint('BOTTOM', self, 'BOTTOM', 0, XP_INSET)
  local currentProgress = (self.currentXP < self.currentCost) and (self.currentXP / self.currentCost) or 1
  local projectedProgress = (self.totalXP < self.totalCost) and (self.totalXP / self.totalCost) or 1
  if self.actualLevel ~= self.level then
    r3, g3, b3 = 0, 1, 1
  end

  print('|cFFFF4400', currentProgress, projectedProgress, self.currentLevel, self.totalLevel)
  if self.level <= WEAPON_MAX_TRAITS then
    self.CurrentProgress.animateFrom = currentHeight or 1
    self.CurrentProgress.animateTo = currentProgress * maxHeight
    self.CurrentProgress:Show()
  else
    self.CurrentProgress:Hide()
  end

  local nextLevel = (self.totalLevel ~= self.currentLevel)

  if self.totalXP ~= self.currentXP or nextLevel then
    print('project', 'xp test=', (self.totalXP ~= self.currentXP), 'lvl test=', (self.totalLevel ~= self.currentLevel))

    local projectedPos = projectedProgress

    if projectedProgress > currentProgress and (not nextLevel) then
      projectedPos = (projectedProgress - currentProgress)
      self.AdjustedProgress:SetPoint('BOTTOM', self.CurrentProgress, 'TOP', 0, 0)
      print('  set above', projectedPos, self.CurrentProgress:GetPoint(3))
    else
      self.AdjustedProgress:SetPoint('BOTTOM', self, 'BOTTOM', 0 , XP_INSET)
      print('  set under', projectedPos)
    end
    self.AdjustedProgress.animateFrom = self.AdjustedProgress:GetHeight() or 1
    self.AdjustedProgress.animateTo = projectedPos * maxHeight
    self.AdjustedProgress:Show()
  else
    print('nothing to project')
    self.AdjustedProgress:Hide()
  end


  --print(self.CurrentProgress:GetPoint(3))
  --print(self.CurrentProgress:GetSize())

  self.XPBackground:SetPoint('BOTTOMLEFT', self, 'BOTTOMLEFT', XP_INSET, XP_INSET)
  self.XPBackground:SetPoint('TOPRIGHT', self, 'TOPLEFT', XP_INSET + XP_WIDTH, -XP_INSET)
  self.CurrentProgress:SetColorTexture(r3,g3,b3,1)

end

function Artifact:OnLoad()
  print('|cFFFF4400OnLoad()|r', self:GetName(), self:GetID())
  self:RegisterEvent('ARTIFACT_UPDATE')
  self:RegisterEvent('PLAYER_LOGIN')
end

function Artifact:OnEvent(event)
  local itemID, _, _, nextCost = C_ArtifactUI.GetEquippedArtifactInfo()
  print(self:GetID(), '|cFFFF4400OnEvent()|r', event, itemID, '=', self.itemID, (itemID == self.itemID))
  if itemID == self.itemID then
    self:Update()
  end
end

function Artifact:Update()
  if not self.itemID then
    return
  end

  print(self:GetName(), '|ff00FFFFUpdate()|r')
  local r1, g1, b1 = 1, 1, 1  -- top text
  local r2, g2, b2 = 1, 1, 0  -- bottom text
  local levelText = self.level
  local xpText = ShortNumberString(self.currentXP)
  local costText = ShortNumberString(self.currentCost)
  local remainingText  = ShortNumberString(self.currentCost - self.currentXP)

  local maxHeight = self:GetHeight() - 4
  local currentHeight = self.CurrentProgress:GetHeight() or 1
  local offHeight = self.AdjustedProgress:GetHeight() or 1

  if self.actualLevel ~= self.level then
    levelText = self.actualLevel
    r1, g1, b1 = 0, 1, 0
    xpText = ShortNumberString(self.actualXP)
    costText = ShortNumberString(self.actualCost)
    remainingText = ShortNumberString(self.actualCost-self.actualXP)
  --[[elseif self.potentialLevel ~= self.level then
    r1, g1, b1 = 0, 1, 1
    r2, g2, b2 = 0, 1, 1
    costText = ShortNumberString(self.potentialCost)
    remainingText = ShortNumberString(self.potentialCost-self.potentialXP)
    --]]
  end

  self.Level:SetText(levelText)
  self.CurrentXP:SetText( xpText )
  self.RemainingCost:SetText(remainingText)
  self.Level:SetTextColor(r1, g1, b1)
  self.CurrentXP:SetTextColor(r1, g1, b1)

  if self.isEquipped then
    self:SetSize(64,64)
    self:SetNormalTexture([[Interface\Buttons\ButtonHilight-Square]])
    self:GetNormalTexture():SetBlendMode('ADD')
    self:GetNormalTexture():SetVertexColor(0,1,0)
  else
    self:SetSize(48,48)
    self:SetNormalTexture(nil, 'ADD')
  end

  self:UpdateXPBar()

  if self.actualLevel ~= self.currentLevel then
    self:SetNormalTexture([[Interface\Buttons\UI-Quickslot-Depress]], 'ADD')
    self:GetNormalTexture():SetBlendMode('BLEND')
    self:GetNormalTexture():SetVertexColor(1,1,1)
  else
    self:SetNormalTexture(nil, 'ADD')
  end
  self.Icon:SetTexture(self.texture)
end

local XP_SCALING_DURATION = .5
function Artifact:AnimateProgress(region)
  local cTime = GetTime()
  if not region.animateStart then
    region.animateStart = cTime
  end
  local progressTo, progressFrom = region.animateTo, region.animateFrom
    local elapsed = cTime - region.animateStart
  if elapsed >= XP_SCALING_DURATION then
    region:SetHeight(progressTo)
    region.animateTo = nil
    region.animateStart = nil
    region.animateFrom = nil
  else
    local progress = elapsed / XP_SCALING_DURATION
    local height = (progressFrom + (progressTo - progressFrom) * progress)
    --print(self:GetName(), progressTo, progressFrom, (progressTo - progressFrom), ceil(progress*10)/10, ceil(height))
    region:SetHeight(height)
  end
end

function Artifact:OnUpdate(sinceLast)
  if self.CurrentProgress.animateTo then
    self:AnimateProgress(self.CurrentProgress)
  end

  if self.AdjustedProgress.animateTo then
    self:AnimateProgress(self.AdjustedProgress)
  end
end

function Artifact:OnEnter()
  GameTooltip:SetOwner(self, 'ANCHOR_CURSOR')
  GameTooltip:SetText(self.name)
  GameTooltip:AddLine(ShortNumberString(self.currentXP) .. ' / '..ShortNumberString(self.currentCost), 1, 1, 1)
  if self.actualLevel ~= self.level then
    GameTooltip:AddLine(ShortNumberString(self.actualLevel - self.level) .. ' points unlocked', 1, 1, 0)
  end
  if self.currentXP < self.currentCost then
    GameTooltip:AddLine(ShortNumberString(self.currentCost - self.currentXP) .. ' for level ' .. (self.currentLevel+1), 1, 1, 0)
  else
    GameTooltip:AddLine(ShortNumberString(self.actualCost - self.actualXP) .. ' for level ' .. (self.actualLevel+1), 0, 1, 0)
  end
  if self.totalLevel ~= self.actualLevel then
    GameTooltip:AddLine('Level ' .. self.totalLevel .. ' with unspent tokens', 0, 1, 1)
  end

  GameTooltip:Show()
end
function Artifact:OnLeave()
  if GameTooltip:IsOwned(self) then
    GameTooltip:Hide()
  end
end
function Artifact:OnHide()

  if GameTooltip:IsOwned(self) then
    GameTooltip:Hide()
  end
end

function Artifact:OnClick(button, down)
  if self.isEquipped then
    SocketInventoryItem(16)
  else
    if IsShiftKeyDown() then
      SocketContainerItem(self.containerID, self.slotID)
    else

    end
  end
end