view Modules/BuffFrame.lua @ 92:d41c7dc3012f

Added tag v7.1-r91 for changeset fe8806a7b6fc
author Nenue
date Tue, 25 Oct 2016 12:33:20 -0400
parents 6e2cb847c3c6
children caded2668701
line wrap: on
line source
-- Veneer
-- BuffFrame.lua
-- Created: 7/27/2016 8:08 PM
-- %file-revision%
--[[
  Adds progress bars and cooldown swirls to buffbutton frames

  Known Limitations:
  - Individual BuffButton frames are created upon use, making it difficult to do any sort of securestate priming
  - TempEnchant info returns relative values only, and they don't synchronize with aura events
  - BuffButtons can only be hidden/shown by blizzcode, so functions doing that have to be accounted for
--]]


local BUFFS_PER_ROW = 12
local BUFF_BUTTON_SIZE = 48
local BUFF_BUTTON_SPACING_H = 5
local BUFF_BUTTON_SPACING_V = 14
local BUFF_PROGRESS_SIZE = 4
local BUFF_PROGRESS_INSET = 2
local PROGRESS_ANCHOR = 'BOTTOM'
local PROGRESS_PARENT
local PROGRESS_OFFSET = 1

local BUFF_BUTTON_ZOOM = .15
local BORDER_SIZE_L = 2
local BORDER_SIZE_R = 2
local BORDER_SIZE_U = 2
local BORDER_SIZE_D = 2
local BUFF_FRAMES_X = -230
local BUFF_FRAMES_Y = -4

local COUNT_ANCHOR = 'TOPRIGHT'
local COUNT_INSET = 4
local COUNT_PARENT

local DURATION_ANCHOR = 'BOTTOMLEFT'
local DURATION_INSET = 4
local DURATION_PARENT

VeneerBuffFrameMixin = {
  moduleName = 'Buff Frames',
  defaultCluster = 'TOPRIGHT',
  anchorX = BUFF_FRAMES_X,
  anchorY = BUFF_FRAMES_Y,
  anchorPoint = 'TOPRIGHT',
  Buttons = {},
  DetectedFrames = {},
  AuraCache = {}
}
VeneerBuffFrameButtonMixin = {
}
local plugin = VeneerBuffFrameMixin

local vn = Veneer
local print = DEVIAN_WORKSPACE and function(...) _G.print('BuffFrame', ...) end or function() end
local tprint = DEVIAN_WORKSPACE and function(...) _G.print('Timer', ...) end or function() end

local _G, UIParent = _G, UIParent
local tinsert, tremove, unpack, select, tconcat = table.insert, table.remove, unpack, select, table.concat
local floor, tonumber, format = math.floor, tonumber, string.format
local UnitAura, GetTime, CreateFrame =  UnitAura, GetTime, CreateFrame
local hooksecurefunc = hooksecurefunc

local aurasCache = {}
local skinnedFrames = {}
local pendingFrames = {}
local veneers = {}
local expirationCache = {}
local visibility = {}
local isHooked = {}

plugin.options = {
  nameString = 'Buff Frames',
  {
    name = 'BuffButtonZoom',
    type = 'slider',
    min = 0,
    max = 100,
    fullwidth = true,
  },
  {
    name = 'BuffBorderLeft',
    type = 'slider',
    min = 0,
    max = 16,
  },
  {
    name = 'BuffBorderLeft',
    type = 'slider',
    min = 0,
    max = 16,
  }
}

local OFFSET_PARALLELS = {
  TOP = {'LEFT', 'RIGHT', 'SetHeight'},
  BOTTOM = {'LEFT', 'RIGHT', 'SetHeight'},
  LEFT = {'TOP', 'BOTTOM', 'SetWidth'},
  RIGHT = {'TOP', 'BOTTOM', 'SetWidth'},
}
local ANCHOR_OFFSET_POINT = {
  TOP = 'BOTTOM',
  TOPLEFT = 'BOTTOMRIGHT',
  TOPRIGHT = 'BOTTOMLEFT',
  LEFT = 'RIGHT',
  RIGHT = 'LEFT',
  CENTER = 'CENTER',
  BOTTOM = 'TOP',
  BOTTOMRIGHT = 'TOPLEFT',
  BOTTOMLEFT = 'TOPRIGHT',
}
local ANCHOR_INSET_DELTA = {
  TOP = {0, -1},
  TOPLEFT = {1, -1},
  TOPRIGHT = {-1,-1},
  LEFT = {1, 0},
  BOTTOMLEFT = {1, 1},
  BOTTOM = {0, 1},
  BOTTOMRIGHT = {-1, 1},
  RIGHT = {-1, 0},
  CENTER = {0, 0},
}

-- Associates skinning elements with said button
local surrogates = {
  ['Show'] = false,
  ['Hide'] = false,
  ['SetText'] = false,
  ['SetVertexColor'] = function(self, region, r, g, b, a)
    if not self.progress then
      return
    end

    region:Hide()
    tprint('|cFF0088FFborder:SetVertexColor|r', r,g,b,a)
    self.progress.fg:SetColorTexture(r,g,b,a)
    self.border:SetColorTexture(r,g,b,a)
    self.border:Show()
  end,
}
local DoRegionHooks =  function (veneer, region)

  if region then
    --print('hooking', region:GetName())
    region:ClearAllPoints()
    for method, callback in pairs(surrogates) do
      if type(region[method]) == 'function' then

        --print(method, type(callback))
        local func
        if callback then
          hooksecurefunc(region, method, function(self, ...)
            --tprint('|cFF00FFFF'.. region:GetName().. ':', method)
            region:ClearAllPoints()
            callback(veneer, region, ...)
          end)
        else
          hooksecurefunc(region, method, function(self,...)
            tprint('|cFF0088FF'.. self:GetName().. ':', method)
            self:ClearAllPoints()
            veneer:Show()
            veneer[method](veneer, ...)

            if self:GetName():match('Debuff.+Count') then

              print('|cFF00FFFF'.. self:GetName().. ':'.. method, '->', veneer:GetName()..':'..method..'(', ...,')')
              print(veneer:IsVisible(),veneer:GetStringWidth(),veneer:GetText())
              print(veneer:GetTop(), veneer:GetLeft())
              print(veneer:GetPoint(1))
            end

          end)
        end
      end
    end
  end
end
function VeneerBuffFrameMixin:Acquire(target)

  local frame = self.Buttons[target]
  if not (self.Buttons[target]) then
    local name = target:GetName()
    local id = target:GetID()
    print('|cFF88FF00Creating', name,'Veneer')

    frame = vn:Acquire(target, 'VeneerBuffTemplate')

    frame.progress[OFFSET_PARALLELS[PROGRESS_ANCHOR][3]](frame.progress, BUFF_PROGRESS_SIZE + (BUFF_PROGRESS_INSET * 2))
    print(BUFF_PROGRESS_SIZE + (BUFF_PROGRESS_INSET * 2))

    frame.progress:ClearAllPoints()
    frame.progress:SetPoint(ANCHOR_OFFSET_POINT[PROGRESS_ANCHOR], PROGRESS_PARENT or frame.border, PROGRESS_ANCHOR,
      (ANCHOR_INSET_DELTA[PROGRESS_ANCHOR][1] * PROGRESS_OFFSET * -1),
      (ANCHOR_INSET_DELTA[PROGRESS_ANCHOR][2] * PROGRESS_OFFSET * -1))
    frame.progress:SetPoint(OFFSET_PARALLELS[PROGRESS_ANCHOR][1], frame.border, OFFSET_PARALLELS[PROGRESS_ANCHOR][1], 0, 0)
    frame.progress:SetPoint(OFFSET_PARALLELS[PROGRESS_ANCHOR][2], frame.border, OFFSET_PARALLELS[PROGRESS_ANCHOR][2], 0, 0)

    print(frame.progress:GetPoint(1))
    print(frame.progress:GetPoint(2))
    print(frame.progress:GetPoint(3))
    frame.progress:Show()

    frame.progress.bg:ClearAllPoints()
    frame.progress.bg:SetAllPoints(frame.progress)

    frame.progress.fg:ClearAllPoints()
    frame.progress.fg:SetPoint('BOTTOMLEFT', BUFF_PROGRESS_INSET,BUFF_PROGRESS_INSET)
    frame.progress.fg:SetPoint('TOP', 0, -BUFF_PROGRESS_INSET)
    --frame.count:ClearAllPoints()
    --frame.count:SetPoint('TOPRIGHT', frame,'TOPRIGHT', -3, -3)


    frame.duration:ClearAllPoints()
    frame.duration:SetPoint(DURATION_ANCHOR, DURATION_PARENT or frame, DURATION_ANCHOR,
      (ANCHOR_INSET_DELTA[DURATION_ANCHOR][1] * DURATION_INSET),
      (ANCHOR_INSET_DELTA[DURATION_ANCHOR][2] * DURATION_INSET))

    frame.count:ClearAllPoints()
    frame.count:SetPoint(COUNT_ANCHOR, COUNT_PARENT or frame, COUNT_ANCHOR,
      (ANCHOR_INSET_DELTA[COUNT_ANCHOR][1] * COUNT_INSET),
      (ANCHOR_INSET_DELTA[COUNT_ANCHOR][2] * COUNT_INSET))

    frame.underlay:SetParent(UIParent)
    frame.underlay:SetFrameStrata('BACKGROUND')
    frame.border:SetColorTexture(0,0,0,1)
    frame.border:SetPoint('TOPLEFT', frame, 'TOPLEFT', -BORDER_SIZE_L, BORDER_SIZE_U)
    frame.border:SetPoint('BOTTOMRIGHT', frame, 'BOTTOMRIGHT', BORDER_SIZE_R, -BORDER_SIZE_D)
    frame.border:Show()

    self.Buttons[target] = frame
  end
  return frame
end

function VeneerBuffFrameMixin:OnLoad()
  Veneer:AddHandler(self, self.defaultCluster)
end

function VeneerBuffFrameMixin:Setup()


  hooksecurefunc("BuffFrame_Update", function(...) self:OnBuffFrameUpdate(...) end)
  hooksecurefunc("AuraButton_UpdateDuration", function(...) self:OnUpdateDuration(...) end)
  hooksecurefunc("AuraButton_Update", function(...) self:OnAuraButton_Update(...) end)
  hooksecurefunc("BuffFrame_UpdateAllBuffAnchors", function(...) self:OnUpdateAllBuffAnchors(...) end)
  hooksecurefunc("TemporaryEnchantFrame_Update", function(...) self:OnTemporaryEnchantFrameUpdate(...) end)
  for i = 1, 3 do
    self:SetupButton('TempEnchant'..i)
    _G['TempEnchant'..i..'Border']:SetVertexColor(0.5,0,1,1)
  end
end


function VeneerBuffFrameMixin:SetupButton (name)
  local frame = _G[name ]
  if self.DetectedFrames[frame] then
    --print('|cFFFF4400Attempting to skin a frame that already went through.|r')
    return
  end
  --print('|cFFFFFF00Adopting', name)

  local icon = _G[name .. 'Icon']
  local border = _G[name .. 'Border']
  local count = _G[name .. 'Count']
  local duration = _G[name .. 'Duration']
  local veneer = self:Acquire(frame)
  local offset = BUFF_BUTTON_ZOOM/2

  self.DetectedFrames[frame] = frame
  frame:SetSize(BUFF_BUTTON_SIZE,BUFF_BUTTON_SIZE)
  icon:SetTexCoord(offset, 1 - offset, offset, 1 - offset)



  DoRegionHooks(veneer, border)
  if border then
    local color = DebuffTypeColor["none"]
    if aurasCache[frame] and aurasCache[frame][5] then
      color = DebuffTypeColor[aurasCache[frame][5]]
    end
    veneer.progress.fg:SetColorTexture(color.r,color.g,color.b)
    veneer.border:SetColorTexture(0,0,0,1)
    veneer.border:Show()
  else
    veneer.border:SetColorTexture(0,0,0,1)
    veneer.border:Show()
  end

  if count and count:GetText() then
    count:ClearAllPoints()
    veneer.count:SetText(count:GetText())
  end
  if duration then
    duration:ClearAllPoints()
  end




  hooksecurefunc(frame, "Hide", function(self)
    local isVisible = self:IsVisible()
    if isVisible ~= visibility[self] then
      visibility[self] = isVisible
    end
    veneer:Hide()
    veneer.underlay:Hide()
  end)

  hooksecurefunc(frame, 'Show', function(self)
    veneer:Show()
    local isVisible = self:IsVisible()
    if isVisible ~= visibility[self] then
      print('|cFFFFFF00SHOW|r', self:GetName())
      visibility[self] = isVisible
    end
    veneer.underlay:Show()
  end)

end


--- Set widgets to reflect the passed parameters
function VeneerBuffFrameMixin:UpdateButton (frame, duration, expires)
  local veneer = self:Acquire(frame)
  -- is it a new button?
  if not self.DetectedFrames[frame] then
    self:SetupButton(frame:GetName())
  end

  --[[
  if frame.count then
    frame.count:SetText('test')
    frame.count:Show()
  end
  --]]
  local name, rank, icon, count, _, duration, expires = UnitAura(frame.unit, frame:GetID(), frame.filter)


  if expires and duration then
    if duration ~= 0 then
      local startTime = (expires - duration)
      local endTime = expires or 0
      print('|cFF0088FF'..frame:GetName()..'|r', duration, expires)
      veneer.progress:Show()
      veneer.elapsed = 0
      veneer.progress:SetScript('OnUpdate', function(self, elapsed)
        veneer.elapsed = veneer.elapsed + elapsed

        local w = floor(veneer.progress:GetWidth()+.5) - (BUFF_PROGRESS_INSET*2)
        local t = GetTime()
        local progress = (t - startTime) / duration

        local nw = (w - (w * progress))
        if veneer.elapsed >= 0.25 then

          tprint(t, startTime, floor(progress*100), w * progress, nw, w)
          veneer.elapsed = 0.25 - veneer.elapsed
        end
        if (progress >= 1) or not frame:IsVisible() then
          veneer.startTime = nil
          self:Hide()
          self:SetScript('OnUpdate', nil)
        else
          self.fg:SetWidth(nw)
        end
      end)

      veneer.cooldown:Show()
      veneer.cooldown:SetCooldown(startTime, duration)
    else
      print('|cFF00FF88'..frame:GetName()..'|r', 'duration zero')
      veneer.progress:SetScript('OnUpdate', nil)
      veneer.progress:Hide()
      veneer.cooldown:Hide()
    end

    if count and count > 1 then
      veneer.count:SetText(count)
      veneer.count:Show()
      frame.count:ClearAllPoints()
    else
      veneer.count:Hide()
    end


  else
    veneer.progress:Hide()
    veneer.cooldown:SetCooldown(0,0)
    veneer.cooldown:Hide()
    print('|cFF88FF00'..frame:GetName()..'|r', 'nil duration')
  end
  veneer:Show()
end


--- Provides the number of changed indices for use in deciding between partial and full veneer updates
function VeneerBuffFrameMixin:ButtonHasChanged (frame, ...)
  aurasCache[frame] = aurasCache[frame] or {}
  local hasChange = 0
  local numVals = select('#',...)
  for i = 1,   numVals do
    local arg = select(i, ...)
    if aurasCache[frame][i] ~= arg then
      hasChange = hasChange + 1
    end
    aurasCache[frame][i] = arg
  end
  return hasChange
end

function VeneerBuffFrameMixin:OnAuraButton_Update (name, index, filter)
  local bName = name..index
  local frame = _G[bName]
  if frame and frame:IsVisible() then
    -- if the name or expirationTime changed

      if not skinnedFrames[frame] then
        tinsert(pendingFrames, frame)
      end
      expirationCache[name] = frame.expirationTime
      self:UpdateButton(frame)


  end
end

function VeneerBuffFrameMixin:OnUpdateAllBuffAnchors ()
  local todo = {}
  if #pendingFrames >= 1 then

    print('|cFFBBFF00AllBuffAnchors|r', #pendingFrames)
    while pendingFrames[1] do
      local frame = tremove(pendingFrames)
      tinsert(todo, frame:GetName())

      -- re-apply custom anchors
    end
    print(tconcat(todo, ', '))
  end
  --BuffButton1
  --DebuffButton1
  --todo: separate frame groups and iterate over them at appropriate times
  if BuffButton1 then

    TempEnchant1:SetPoint('TOPRIGHT', BuffButton1, 'TOPRIGHT', BuffButton1:GetWidth()+4, 0)
  end

  local lastBuff, topBuff
  local numBuffs = 0
  local numColumns = 1
  local maxColumn = 1
  for i = 1, BUFF_ACTUAL_DISPLAY do
    local buff = _G['BuffButton'..i]
    if buff then
      numBuffs = numBuffs + 1
      buff:ClearAllPoints()
      if mod(numBuffs,BUFFS_PER_ROW) == 1 then
        if numBuffs == 1 then
          buff:SetPoint('TOPRIGHT', UIParent, 'TOPRIGHT', BUFF_FRAMES_X, BUFF_FRAMES_Y)
          plugin.currentTop = buff
        else
          buff:SetPoint('TOPRIGHT', topBuff, 'BOTTOMRIGHT', 0, -BUFF_BUTTON_SPACING_V)
        end
        numColumns = 1
        topBuff = buff
      else
        buff:SetPoint('TOPRIGHT', lastBuff, 'TOPLEFT', -BUFF_BUTTON_SPACING_H, 0)
        numColumns = numColumns + 1
      end
      if numColumns > maxColumn then
        maxColumn = numColumns
        plugin.currentLeft = buff
      end
      lastBuff = buff
    end
  end

  numBuffs = 0
  for i = 1, DEBUFF_ACTUAL_DISPLAY do
    local debuff = _G['DebuffButton'..i]
    if debuff then
      numBuffs = numBuffs + 1
      if numBuffs == 1 then
        if topBuff then
          debuff:SetPoint('TOPRIGHT', topBuff, 'BOTTOMRIGHT',  0, -BUFF_BUTTON_SPACING_V)
        else
          debuff:SetPoint('TOPRIGHT', UIParent, 'TOPRIGHT', -120, -6)
        end
        topBuff = debuff
      elseif mod(numBuffs, BUFFS_PER_ROW) == 1 then
        debuff:SetPoint('TOPRIGHT', topBuff, 'BOTTOMRIGHT',  0, -BUFF_BUTTON_SPACING_V)
        topBuff = debuff
      else
        debuff:SetPoint('TOPRIGHT', lastBuff, 'TOPLEFT', -BUFF_BUTTON_SPACING_H, 0)
      end
      lastBuff = debuff
    end
  end

  if lastBuff then
    plugin.currentBottom = lastBuff
  end

  self.Background:ClearAllPoints()
  self.Background:SetPoint('TOPRIGHT', plugin.currentTop, 'TOPRIGHT', 4, 4)
  self.Background:SetPoint('BOTTOM', plugin.currentBottom, 'BOTTOM', 0, -4)
  self.Background:SetPoint('LEFT', plugin.currentLeft, 'LEFT', -4, 0)
end
function VeneerBuffFrameMixin:UpdateConfigLayers (configMode)
  self:SetShown(configMode)
end
function VeneerBuffFrameMixin:OnUpdateDuration (frame, timeLeft)
  local veneer = self:Acquire(frame)
  local hours =  floor(timeLeft/3600)
  local minutes = floor(mod(timeLeft, 3600)/60)
  local seconds = floor(mod(timeLeft, 60))
  local timeString = '%ds'
  if timeLeft > 3600 then
    timeString = format('%d:%02d', hours, minutes)
  elseif timeLeft > 60 then
    timeString = format('%d:%02d', minutes, seconds)
  else
    timeString = format('%d', seconds)
  end

  if timeLeft < 10 then
    if not veneer.duration.getHuge then
      veneer.duration.getHuge = true
      veneer.duration:SetFontObject(VeneerNumberFontLarge)
      veneer.duration:SetTextColor(1,1,0,1)
    end
  else
    if veneer.duration.getHuge then
      veneer.duration.getHuge = nil
      veneer.duration:SetFontObject(VeneerNumberFont)
      veneer.duration:SetTextColor(1,1,1,1)
    end
  end

  veneer.duration:SetText(timeString)
end


-- Obtains the first instance of Tenchant use

function VeneerBuffFrameMixin:OnTemporaryEnchantFrameUpdate (...)
  local numVals = select('#', ...)
  local numItems = numVals / 4
  if numItems >= 1 then
    for itemIndex = numItems, 1, -1 do
      local frame = _G['TempEnchant'..itemIndex]
      local hasEnchant, timeRemaining, enchantCharges = select((4 * (itemIndex -1)) + 1, ...)


      if hasEnchant then
        local endTime = floor(GetTime()*1000) + timeRemaining


        --print(endTime)
        if endTime ~= expirationCache[frame] then
          if expirationCache[frame] then
            print(endTime, expirationCache[frame], endTime - expirationCache[frame])
          end
          expirationCache[frame] = endTime
          print('push tempenchant timer update', timeRemaining / 1000, GetTime()+(timeRemaining/1000))
          UpdateVeneer(frame, timeRemaining/1000, GetTime()+(timeRemaining/1000))
        end
      else
        self:Acquire(frame):Hide()
      end

    end

  end

end
function VeneerBuffFrameMixin:OnBuffFrameUpdate ()
end


function VeneerBuffFrameButtonMixin:OnLoad()

  self.duration = self.progress.duration
  self.count = self.overlay.count
  self.border = self.underlay.bg

  VeneerBuffFrame.ConfigLayers = VeneerBuffFrame.ConfigLayers or {}
  self.configIndex = #VeneerBuffFrame.ConfigLayers
  for i, region in ipairs(self.ConfigLayers) do
    tinsert(VeneerBuffFrame.ConfigLayers, region)
  end

  self.configIndexEnd = #VeneerBuffFrame.ConfigLayers
end

-- The TempEnchant frames are hardcoded in the base FrameXML, so get them now