view ClassPlan.lua @ 35:26dfa661daa7

WorldPlan: - Quest pins will appear in the flight map. They follow the filter settings applied from the world map. - Reward filter toggle changed to clear out other reward filters. The assumption being that one is most often looking only for that particular type of quest when they go to use it. - Fixed filter bar info falling out of sync after player-triggered world map updates. - Code stuff: -- Quest pin shown-state management makes better use of OnShow OnHide handlers, SetShown is toggled and it all goes from there -- WorldQuests module re-factored outside of the top level frame script. ClassPlan: - Available missions are now recorded; the mission list can be toggled between in-progress and available by clicking the heading.
author Nenue
date Thu, 03 Nov 2016 17:29:15 -0400
parents e8679ecb48d8
children 589c444d4837
line wrap: on
line source
local wipe, tinsert, sort = table.wipe, tinsert, table.sort
local pairs, ipairs = pairs, ipairs
local floor, mod, time = floor, mod, time
local max, min = math.max, math.min
local GetTime = GetTime
local GI_currentTime = time()
local print = DEVIAN_WORKSPACE and function(...) print('ClassPlan', ...) end or nop

local CG_GetBuildings = C_Garrison.GetBuildings
local CG_GetFollowerShipments = C_Garrison.GetFollowerShipments
local CG_GetLooseShipments = C_Garrison.GetLooseShipments
local CG_GetTalentTrees = C_Garrison.GetTalentTrees
local CG_GetCompleteTalent = C_Garrison.GetCompleteTalent
local CG_GetLandingPageShipmentInfo = C_Garrison.GetLandingPageShipmentInfo
local CG_GetLandingPageShipmentInfoByContainerID = C_Garrison.GetLandingPageShipmentInfoByContainerID

local CP_REPLACE_LANDINGPAGE = true
local CP_HEADER_SIZE = 24
local CP_BACKGROUND_COLOR = {
  inProgress = {0, 0, 0, 0.5},
  shipmentsReady = {0, 0, 0, 0.25},
  complete = {0.5, 0.5, 0.5, 0.5}
}

local GetTimeLeftString = function(timeLeft)
  local days = floor(timeLeft/(24*3600))
  local hours = floor(mod(timeLeft, (24*3600)) / 3600)
  local minutes = floor(mod(timeLeft, 3600) / 60)
  local seconds = mod(timeLeft, 60)
  if days >= 1 then
    return (days .. 'd' .. ' ') .. ((hours > 0) and (hours .. 'h') or '')
  else
    return ((hours > 0) and (hours .. 'h') or '') .. ((minutes > 0) and (' ' ..minutes .. ' min') or '')
  end
end


ClassOrderPlanCore = {
  events = {},
  freeBlocks = {},
  characterButtons = {},
  blocks = {},
  sortedItems = {},
  timers = {},
  shipments = {},
  playerFirst = false,
  prototypes = {},
  Queued = {}
}
local MissionList = {
  templateName = 'ClassPlanMissionEntry',
  listKey = {'missions', 'available'},
  listTitle = {'In Progress', 'Available'},

  point = 'TOPLEFT',
  relativePoint ='TOPLEFT',
  events = {
    'GARRISON_MISSION_LIST_UPDATE',
    'GARRISON_LANDINGPAGE_SHIPMENTS'},
}
local ShipmentList = {
  templateName = 'ClassPlanShipmentEntry',
  listKey = {'shipments'},
  listTitle = {'Work Orders'},
  events = {
    'GARRISON_MISSION_LIST_UPDATE',
    'GARRISON_LANDINGPAGE_SHIPMENTS',
    'GARRISON_TALENT_UPDATE',
    "GARRISON_TALENT_COMPLETE",
    "GARRISON_SHIPMENT_RECEIVED",
    'GARRISON_FOLLOWER_LIST_UPDATE',
    'GARRISON_SHOW_LANDING_PAGE'},
}
local SharedHandlers = {
  numBlocks = 0,
  isStale = true,
  maxItems = 10
}
local SharedEntry = {}
local ShipmentEntry = {}
local MissionEntry = {}

local ClassPlan = ClassOrderPlanCore

function ClassPlan:OnLoad ()
  self:RegisterEvent('PLAYER_LOGIN')
  self:RegisterEvent('ADDON_LOADED')
  self:RegisterEvent('PLAYER_REGEN_ENABLED')
  self:RegisterEvent('PLAYER_REGEN_DISABLED')
  self:RegisterForDrag('LeftButton')
  self:SetMovable(true)
  self:SetToplevel(true)


  SLASH_CLASSPLAN1 = "/classplan"
  SLASH_CLASSPLAN2 = "/cp"
  SlashCmdList.CLASSPLAN = function(args)
    self:Toggle()
  end

end

function ClassPlan:GetCurrentProfile()
  WorldPlanData.OrderHall = WorldPlanData.OrderHall or {}
  local db = WorldPlanData.OrderHall
  self.data = db

  local characters = db.characters or {}
  db.characters = characters

  local name, realm = UnitName('player')
  realm  = realm or GetRealmName()
  local profileName = name .. '-' .. realm

  self.profile = characters[profileName] or {}
  self.characters = characters
  characters[profileName] = self.profile


  local classColor = RAID_CLASS_COLORS[select(2, UnitClass('player'))]
  local className = UnitClass('player')

  print('|cFFFFFF00Loaded:|r', classColor.hex, className, profileName)
  self.Background:SetColorTexture(classColor.r, classColor.g, classColor.b, 0.5)
  self.profile.classColor = classColor
  self.profile.className = className
  self.profile.characterName = name
  self.profile.characterRealm = realm
  return self.profile
end

function ClassPlan:SetupHandler(handler)
  print('|cFF00FF00'..handler:GetName()..' loaded')
  for i, event in ipairs(handler.events) do
    print('|cFF00FF00  event', event)
    handler:RegisterEvent(event)
  end
  for index, listKey in ipairs(handler.listKey) do
    self.profile[listKey] = self.profile[listKey] or {}
    local listTitle = handler.listTitle[index]
    setmetatable(self.profile[listKey], { __tostring = listTitle })
  end
  handler:SetList(1)
  handler.sortedItems = {}
end

function ClassPlan:OnEvent (event, arg)
  print(event, arg)
  if event == 'PLAYER_REGEN_DISABLED' then
    if self:IsVisible() then
      self.combatHide = true
      self:SetShown(false)
    end

  elseif event == 'PLAYER_REGEN_ENABLED' then
    if self.combatHide == true then
      self.combatHide = nil
      self:SetShown(true)
    end
  elseif event == 'ADDON_LOADED' then
    if arg == 'Blizzard_GarrisonUI' then
      self:Reanchor()
    end
  elseif event == 'PLAYER_LOGIN' then
    if not self.initialized then
      self:Setup()
    end
  end
end

function ClassPlan:Setup()
  if IsLoggedIn() then
    print('|cFFFFFF00'..self:GetName()..':Setup()|r')

    self:GetCurrentProfile()
    for _, handler in ipairs(self.Handlers) do
      self:SetupHandler(handler)
    end
    self.initialized = true
    self:SetShown(self.data.IsShown)
  end
end


--- Update space

  local max = math.max
  function ClassPlan:Update()
    print('|cFF00FFFFRefresh()|r')
    self.currentHeight = 0
    for index, handler in pairs(self.Handlers) do
      if handler.isStale then
        print('  |cFF00FF00'..index..' '..handler:GetName()..'|r')
        local sortedItems = handler.sortedItems
        local activeKey = handler.activeKey

        handler.profile = self.profile[handler.activeKey]
        handler.currentTime = GI_currentTime
        handler:GetPlayerData(self.profile)
        wipe(sortedItems)
        for key, profile in pairs(self.data.characters) do
          print('profile', key, activeKey)
          local profileList = profile[activeKey]
          if profileList and #profileList >= 1 then
            local classColor = profile.classColor or RAID_CLASS_COLORS['HUNTER']
            local isMine = (profile == self.profile)
            for index, data in ipairs(profileList) do
              data.classColor = classColor
              data.profileKey = key
              data.isMine = isMine
              if handler.OnGetItem then
                handler:OnGetItem(data)
              end
              tinsert(sortedItems, data)
            end
          end
        end

        if handler.SortHandler then
          sort(sortedItems, handler.SortHandler)
        end

      end
      handler.isStale = nil
      local itemsHeight = handler:UpdateItems()
      self.currentHeight = max(itemsHeight, self.currentHeight)

    end

    local index = 1
    for id, profile in pairs(self.data.characters) do
      local button = self.characterButtons[index]
      if not button then
        button = CreateFrame('Button', nil, self, 'ClassOrderPlanCharacterButton')
        button:SetID(index)
        self.characterButtons[index] = button

        if not self.lastButton then
          button:SetPoint('BOTTOMLEFT', self, 'TOPLEFT', 0, 0)
        else
          button:SetPoint('BOTTOMLEFT', self.lastButton, 'BOTTOMRIGHT', 2, 0)
        end
        self.lastButton = button
      end
      if not profile.characterName then
        profile.characterName, profile.characterRealm = id:match("%(.+)%-(.+)^")
      end

      button.className = profile.className
      button.classColor = profile.classColor
      button.characterName = profile.characterName
      button.characterRealm = profile.characterRealm
      button.hideItems = (profile.showItems == false) and (profile ~= self.profile)
      button.isMine = (profile == self.profile)
      button:Update()
      button:Show()
      index = index + 1
    end


    self.isStale = nil
    self:Reanchor()
    self:SetHeight(self.currentHeight + CP_HEADER_SIZE)
  end


function ClassPlan:Toggle()
  if self:IsShown() then
    self:Hide()
  else
    self:Show()
  end

  if self.data then
    self.data.IsShown = self:IsShown()
  end
end

function ClassPlan:OnUpdate()
  if self.isStale then
    print('|cFFFF4400An illusion! What are you hiding?|r')
    self:Update()
  end
end

function ClassPlan:OnShow()
  print('|cFF00FFFFShow()')
  if self.isStale then
    self:Update()
  end
  self:Reanchor()
end

function ClassPlan:OnHide()
  print('|cFF00FFFFHide()')
end

function ClassPlan:Reanchor()
  self:ClearAllPoints()
  self:SetPoint('CENTER', self.data.positionX, self.data.positionY)

  for index, frame in ipairs(self.Handlers) do
    frame:Reanchor()

    local ListTab = frame.ListTab
    if ListTab then
      ListTab:ClearAllPoints()
      ListTab:SetPoint('TOPLEFT', frame, 'TOPLEFT', 0, CP_HEADER_SIZE)
      ListTab:SetPoint('BOTTOMRIGHT', frame, 'TOPRIGHT', 0, 0)
      ListTab.Label:SetText(frame.listTitle[frame.currentListIndex])
      ListTab:Show()
      print(ListTab:GetSize())
    end

  end
end

function ClassPlan:OnDragStart()
  self:StartMoving()
end
function ClassPlan:OnDragStop()

  self:StopMovingOrSizing()
  local x,y = self:GetCenter()
  if x and y then
    x = (x - GetScreenWidth()/2)
    y = (y - GetScreenHeight()/2) * -1
    self.data.positionX, self.data.positionY = x,y
    print('saving positions:', x, y)
  end
end

function SharedHandlers:SetList(index)
  if not index then
    if self.currentListIndex == #self.listKey then
      index = 1
    else
      index = self.currentListIndex + 1
    end
  end

  print('|cFF0088FF'..self:GetName()..'|r:SetList()', index)
  self.currentListIndex = index
  self.activeKey = self.listKey[index]
  self.activeTitle = self.listTitle[index]

  self.isStale = true
end

function SharedHandlers:OnMouseWheel(delta)
  self.scrollOffset = (self.scrollOffset or 0) - ((delta > 0) and 1 or -1)
  self:UpdateItems()
end

function SharedHandlers:RequestData()
  print('|cFF0088FF'..self:GetName()..':RequestData()')
  self.isStale = true
end

function SharedHandlers:OnEvent(event, arg)
  if (event == 'GARRISON_MISSION_LIST_UPDATE') and (arg ~= LE_FOLLOWER_TYPE_GARRISON_7_0) then
    -- ignore non-OrderHall updates
    return
  end
  print('|cFF00FF88'..self:GetName()..':OnEvent()|r', event, arg)
  if self:IsVisible() then
    print('|cFF88FF00  frame visible; get busy')
    self:RequestData()
  else
    if not self.NextData then
      print('|cFF88FF00  setting timer')
      self.NextData = C_Timer.NewTimer(0.25, function()
        if self.initialized then
          self:RequestData()
          self.NextData:Cancel()
          self.NextData = nil
          print('|cFF88FF00'..self:GetName()..' clearing timer')
        end

      end)
    end
  end
end
function SharedHandlers:OnUpdate()
  if self.isStale then
    self:GetParent():Update()
  end
end


-- Stuff set on every list item
function SharedHandlers:SetOwnerData (self, data)
  local name, realm = string.match(data.profileKey, "(.+)%-(.+)")
  local ownerText = '|c'.. data.classColor.colorStr .. name .. '|r'
  self.Owner:SetText(ownerText)
  self.Name:SetText(self.name)
  self.Name:SetTextColor(data.classColor.r, data.classColor.g, data.classColor.b)
end

function SharedHandlers:Acquire(id)
end
function SharedHandlers:FreeBlock (block)
end

function SharedHandlers:UpdateItems()

  self.MoreItemsUp:Hide()
  self.MoreItemsDown:Hide()

  local sortedItems = self.sortedItems
  local scrollOffset = self.scrollOffset or 0
  local numItems = #sortedItems
  if (not sortedItems[scrollOffset+1]) or (numItems <= self.maxItems) then
    scrollOffset = 0
  elseif (numItems > self.maxItems) and (scrollOffset > (numItems - self.maxItems)) then
    scrollOffset = (numItems - self.maxItems)
  end


  self.blocks = self.blocks or  {}
  local blocks = self.blocks

  local lastProfile
  local totalHeight = 0
  self.lastBlock = nil
  self.numActive = 0
  for i = 1, self.maxItems do
    local index = scrollOffset + i
    local data = sortedItems[index]
    if not data then
      break
    end


    local block = blocks[i]
    if not block then
      block = CreateFrame('Button', nil, self, self.templateName)
      block.listType = self.activeKey
      block.handler = self
      self.numBlocks = self.numBlocks + 1
      blocks[i] = block
    end
    block:SetID(index)

    print('RefreshItem', block)
    self.numActive = self.numActive + 1

    if self.lastBlock then
      block:SetPoint('TOPLEFT', self.lastBlock, 'BOTTOMLEFT', 0, 0)
      print('--', index, data.isComplete, data.missionEndTime, data.name)
    else
      block:SetPoint('TOPLEFT', 0, 0)
      print('--top')
    end
    self.lastBlock = block

    totalHeight = totalHeight + block:GetHeight()
    block.lastProfile = lastProfile
    -- blot out arbitrary flags
    block.offerEndTime = nil
    block.missionEndTime = nil
    block.creationTime = nil
    block.duration = nil
    block.throttle = 5

    for k,v in pairs(data) do
      if type(block[k]) ~= 'function' then
        block[k] = v
      end
    end

    block:Update()
    self:SetOwnerData(block, data)

    block:Show()
    lastProfile = data.profileKey
  end

  if self.numActive < numItems then
    if scrollOffset < (numItems - self.maxItems) then
      self.MoreItemsDown:Show()
    end
    if scrollOffset > 0 then
      self.MoreItemsUp:Show()
    end
  end

  for i = self.numActive + 1, self.numBlocks do
    if blocks[i] then
      blocks[i]:Hide()
    end
  end

  self.scrollOffset = scrollOffset
  self:Reanchor()

  return totalHeight
end


function ShipmentList:Reanchor()
  print('|cFF00FFFF'..self:GetName()..':Reanchor|r')
  self:SetPoint('TOPLEFT', 0, -24)
  self:SetPoint('BOTTOMRIGHT', -ClassOrderPlan:GetWidth()/2, 0)
end



do
  local ShipmentsInfo = {}
  local AddShipmentInfo = function(shipmentType, name, texture, shipmentCapacity, shipmentsReady, shipmentsTotal, creationTime, duration, timeleftString, itemName, itemIcon, itemQuality, itemID, followerID)
    -- early login queries may return empty tables, causing the sorter to compare nil
    if not creationTime then
      return
    end
    --print(shipmentType, name, shipmentCapacity, shipmentsReady, shipmentsTotal, creationTime, duration, timeleftString)
    tinsert(ShipmentsInfo,
      {
        shipmentType = shipmentType,
        name = name,
        icon = texture,
        shipmentCapacity = shipmentCapacity,
        shipmentsReady = shipmentsReady,
        shipmentsTotal = shipmentsTotal,
        creationTime = creationTime,
        duration = duration,
        timeleftString = timeleftString,
        itemName = itemName,
        itemIcon = itemIcon,
        itemQuality = itemQuality,
        itemID = itemID,
        followerID = followerID,
      })
  end
  function ShipmentList:GetPlayerData (profile)
    if not profile then
      return false
    end
    local profileList = profile.shipments
    wipe(ShipmentsInfo)

    local garrisonType = LE_GARRISON_TYPE_7_0
    local buildings = CG_GetBuildings(garrisonType);
    local shipmentIndex = 0
    --print('Buildings:')
    for i = 1, #buildings do
      local name, texture, shipmentCapacity, shipmentsReady, shipmentsTotal, creationTime, duration, timeleftString, itemName, itemIcon, itemQuality, itemID = CG_GetLandingPageShipmentInfo(buildingID);
      AddShipmentInfo('Building', name, texture, shipmentCapacity, shipmentsReady, shipmentsTotal, creationTime, duration, timeleftString, itemName, itemIcon, itemQuality, itemID)
    end

    --print('Follower:')
    local followerShipments = CG_GetFollowerShipments(garrisonType);
    for i = 1, #followerShipments do
      local name, texture, shipmentCapacity, shipmentsReady, shipmentsTotal, creationTime, duration, timeleftString, _, _, _, _, followerID = CG_GetLandingPageShipmentInfoByContainerID(followerShipments[i]);
      AddShipmentInfo('Follower', name, texture, shipmentCapacity, shipmentsReady, shipmentsTotal, creationTime, duration, timeleftString, nil, nil, nil, nil, followerID)
    end

    --print('Loose:')
    local looseShipments = CG_GetLooseShipments(garrisonType)
    for i = 1, #looseShipments do
      local name, texture, shipmentCapacity, shipmentsReady, shipmentsTotal, creationTime, duration, timeleftString = CG_GetLandingPageShipmentInfoByContainerID(looseShipments[i]);
      AddShipmentInfo('Misc', name, texture, shipmentCapacity, shipmentsReady, shipmentsTotal, creationTime, duration, timeleftString)
    end

    local talentTrees = CG_GetTalentTrees(garrisonType, select(3, UnitClass("player")));
    -- this is a talent that has completed, but has not been seen in the talent UI yet.
    local completeTalentID = CG_GetCompleteTalent(garrisonType);
    --print('Talents:')
    if (talentTrees) then
      for treeIndex, tree in ipairs(talentTrees) do
        for talentIndex, talent in ipairs(tree) do
          local showTalent = false;
          if (talent.isBeingResearched) or (talent.id == completeTalentID) then
            AddShipmentInfo('Talent', talent.name, talent.icon, 1, (talent.isBeingResearched and 0 or 1), 1, talent.researchStartTime, talent.researchDuration, talent.timeleftString)
          end
        end
      end
    end

    wipe(profileList)
    for index, data in ipairs(ShipmentsInfo) do
      --DEFAULT_CHAT_FRAME:AddMessage(data.shipmentType ..' '.. tostring(data.name) ..' '.. tostring(data.creationTime) ..' '.. tostring(data.duration))
      tinsert(profileList, data)
    end
    self.isStale = true
    return true
  end
end


function SharedEntry:SetTimeLeft(expires, duration)
  self.ProgressBG:Hide()
  self.ProgressBar:Hide()
  if not expires then
    return
  end

  -- calculate here since time isn't available
  local timeLeft = expires - GI_currentTime
  if timeLeft < 0 then
    -- handle being complete

  else
    self.TimeLeft:SetText(GetTimeLeftString(timeLeft))
  end

  if (timeLeft > 0) and duration then
    local progress = (duration - timeLeft) / duration
    local r = ((progress >= .5) and (progress/2)) or 1
    local g = ((progress <= .5) and (progress*2)) or 1
    self.ProgressBG:Show()
    self.ProgressBar:Show()

    self.ProgressBG:SetColorTexture(r,g,0,0.25)
    self.ProgressBar:SetColorTexture(r,g,0,0.5)
    self.ProgressBar:SetWidth(self:GetWidth() * progress)
  end
end

-- Update shipment flags data
local SetActualShipmentTime = function(self)

  if self.isComplete then
    return nil, nil
  end

  local timestamp = time()
  local timeLeft = self.creationTime + self.duration - timestamp
  local duration = self.duration * self.shipmentsTotal
  local justFinished = false
  while (self.shipmentsReady < self.shipmentsTotal) and (timeLeft <= 0) do
    if not self.originalReady then
      self.originalReady = self.shipmentsReady
      self.originalCreationTime = self.creationTime
    end


    self.shipmentsReady = self.shipmentsReady + 1
    self.creationTime = self.creationTime + self.duration
    timeLeft = timeLeft + self.duration
    print('|cFF00FF88udpating '..self.name..'|r', 'timeLeft:', timeLeft, 'shipments:', self.shipmentsReady, self.shipmentsTotal)
  end

  if (timeLeft <= 0) and (not self.isBeingResearched) then
    self.isComplete = true
    self.isStale = true
  end


  local expires = (self.originalCreationTime or self.creationTime) + duration

  return expires, duration
end

function ShipmentList:OnGetItem (data)
  print('OnGetItem()')
  if data.shipmentsTotal then
    SetActualShipmentTime(data)
  end
end

ShipmentList.SortHandler = function(a, b)
  if b.isComplete ~= a.isComplete then
    return a.isComplete and true or false
  elseif a.shipmentsReady or b.shipmentsReady then
    return (a.shipmentsReady or 0) > (b.shipmentsReady or 0)
  else
    return (a.creationTime) < (b.creationTime)
  end
end

function ShipmentList:OnLoad()
  C_Garrison.RequestLandingPageShipmentInfo();
end
function  ShipmentList:OnShow()
  print('|cFF00FF88'..self:GetName()..':OnShow()|r')
  C_Garrison.RequestLandingPageShipmentInfo()
end

function ShipmentEntry:OnLoad()
  MissionEntry.OnLoad(self)
end


function ShipmentEntry:Update()
  print('|cFF0088FF'.. self.name..'|r:Update()')
  self.Icon:SetTexture(self.icon)
  self.Count:SetText(self.shipmentsReady)
  self.Done:SetShown(self.shipmentsReady and (self.shipmentsReady >= 1))

  -- flag as complete

  local bgColor = CP_BACKGROUND_COLOR.inProgress
  if ( self.shipmentsReady >= self.shipmentsTotal ) and (not self.isBeingResearched) then
    self.Swipe:SetCooldownUNIX(0, 0);
    self.Done:Show();
    bgColor = CP_BACKGROUND_COLOR.complete
  else
    if (self.shipmentsReady >= 1) and (self.shipmentsReady < self.shipmentsTotal) then
      bgColor = CP_BACKGROUND_COLOR.shipmentsReady
    end
    self.Swipe:SetCooldownUNIX(self.creationTime or 0 , self.duration or 0);
  end
  self.Background:SetColorTexture(unpack(bgColor))

  SetActualShipmentTime(self)

  if self.originalReady then
    print('|cFF00FF88'..self.name..'|r', 'starting ready:', self.originalReady, 'starting time:', self.originalCreationTime)
  end
end

function ShipmentEntry:OnUpdate(sinceLast)
  self.throttle = (self.throttle or 1) + sinceLast
  if self.throttle >= 1 then
    self.throttle = self.throttle - 1
  else
    return
  end


  if (self.shipmentsReady and self.shipmentsTotal) and (self.shipmentsReady < self.shipmentsTotal) then
    local expires, duration = SetActualShipmentTime(self)

    if self.isComplete then
      self.TimeLeft:SetText('Complete!')
      self.TimeLeft:SetTextColor(0,1,1)
    elseif self.shipmentsReady >= 1 then
      self:SetTimeLeft(expires, duration)
      self.TimeLeft:SetTextColor(0,1,0)
    else
      self:SetTimeLeft(expires, duration)
      self.TimeLeft:SetTextColor(1,1,1)
    end

  elseif self.isBeingResearched then
    self:SetTimeLeft(self.researchStartTime + self.researchDuration - time(), self.researchDuration)
    self.TimeLeft:SetTextColor(1,1,1)
  else
    self.TimeLeft:SetText('Complete!')
    self.TimeLeft:SetTextColor(0,1,0)
  end

end

function ShipmentEntry:OnEnter()
  if ( self.shipmentsReady and self.shipmentsTotal ) then
    GameTooltip:SetOwner(self, 'ANCHOR_LEFT')
    GameTooltip:AddLine(self.Owner:GetText(), self.Owner:GetTextColor())
    GameTooltip:AddLine(self.shipmentType)
    GameTooltip:AddLine(self.shipmentsReady .. ' of '.. self.shipmentsTotal)
    GameTooltip:Show()
  end
end

function ShipmentEntry:OnLeave()
  if GameTooltip:IsOwned(self) then
    GameTooltip:Hide()
  end
end

function ShipmentEntry:OnClick(button)
  if button == 'RightButton' then
    self.handler:FreeBlock(self)
  end
end




ClassPlanMissionHandler = Mixin(MissionList, SharedHandlers)
ClassPlanMissionEntryMixin = Mixin(MissionEntry, SharedEntry)
ClassPlanShipmentHandler = Mixin(ShipmentList, SharedHandlers)
ClassPlanShipmentEntryMixin = Mixin(ShipmentEntry,SharedEntry)

ClassPlanHeaderMixin = {
  OnClick = function(self)
    self:GetParent():SetList()
    self:GetParent().isStale = true
    ClassOrderPlan:Update()
  end
}

ClassPlanCharacterButtonMixin = {
  Update = function(self)
    print(CLASS_ICON_TCOORDS[self.className:upper()])
    if self.className and CLASS_ICON_TCOORDS[self.className:upper()] then
      self.Icon:SetTexCoord(unpack(CLASS_ICON_TCOORDS[self.className:upper()]))
    end
    self.Icon:SetDesaturated(self.showItems)
    self.SelectGlow:SetShown(self.isMine)
  end
}

function ClassPlanCharacterButtonMixin:OnEnter() end
function ClassPlanCharacterButtonMixin:OnLeave() end
function ClassPlanCharacterButtonMixin:OnClick() end