diff Gear.lua @ 57:01b63b8ed811 v21

total rewrite to version 21
author yellowfive
date Fri, 05 Jun 2015 11:05:15 -0700
parents
children ee701ce45354
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Gear.lua	Fri Jun 05 11:05:15 2015 -0700
@@ -0,0 +1,745 @@
+local Amr = LibStub("AceAddon-3.0"):GetAddon("AskMrRobot")
+local L = LibStub("AceLocale-3.0"):GetLocale("AskMrRobot", true)
+local AceGUI = LibStub("AceGUI-3.0")
+
+local _gearTabs
+local _activeTab
+
+-- Returns a number indicating how different two items are (0 means the same, higher means more different)
+local function countItemDifferences(item1, item2)
+    if item1 == nil and item2 == nil then return 0 end
+    
+    -- different items (id + bonus ids + suffix, constitutes a different physical drop)
+    if Amr.GetItemUniqueId(item1, true) ~= Amr.GetItemUniqueId(item2, true) then
+		return 1000
+    end
+    
+    -- different upgrade levels of the same item (only for older gear, player has control over upgrade level)
+    if item1.upgradeId ~= item2.upgradeId then
+        return 100
+    end
+    
+    -- different gems
+    local gemDiffs = 0
+    for i = 1, 3 do
+        if item1.gemIds[i] ~= item2.gemIds[i] then
+            gemDiffs = gemDiffs + 1
+        end
+    end
+    
+	-- different enchants
+    local enchantDiff = 0
+    if item1.enchantId ~= item2.enchantId then
+        enchantDiff = 1
+    end
+    
+    return gemDiffs + enchantDiff
+end
+
+-- given a table of items (keyed or indexed doesn't matter) find closest match to item, or nil if none are a match
+local function findMatchingItemFromTable(item, list, bestLink, bestItem, bestDiff, bestLoc, usedItems, tableType)
+	if not list then return nil end
+	
+	for k,v in pairs(list) do
+		local listItem = Amr.ParseItemLink(v)
+		if listItem then
+			local diff = countItemDifferences(item, listItem)
+			if diff < bestDiff then
+				-- each physical item can only be used once, the usedItems table has items we can't use in this search
+				local key = string.format("%s_%s", tableType, k)
+				if not usedItems[key] then
+					bestLink = v
+					bestItem = listItem
+					bestDiff = diff
+					bestLoc = string.format("%s_%s", tableType, k)
+				end
+			end
+			if diff == 0 then break end
+		end
+	end
+	
+	return bestLink, bestItem, bestDiff, bestLoc
+end
+
+-- search the player's equipped gear, bag, bank, and void storage for an item that best matches the specified item
+function Amr:FindMatchingItem(item, player, usedItems)
+	if not item then return nil end
+
+	local equipped = player.Equipped and player.Equipped[player.ActiveSpec] or nil
+	local bestLink, bestItem, bestDiff, bestLoc = findMatchingItemFromTable(item, equipped, nil, nil, 1000, nil, usedItems, "equip")
+	bestLink, bestItem, bestDiff, bestLoc = findMatchingItemFromTable(item, player.BagItems, bestLink, bestItem, bestDiff, bestLoc, usedItems, "bag")
+	bestLink, bestItem, bestDiff, bestLoc = findMatchingItemFromTable(item, player.BankItems, bestLink, bestItem, bestDiff, bestLoc, usedItems, "bank")
+	bestLink, bestItem, bestDiff, bestLoc = findMatchingItemFromTable(item, player.VoidItems, bestLink, bestItem, bestDiff, bestLoc, usedItems, "void")
+
+	if bestDiff >= 1000 then
+		return nil, nil, 1000
+	else
+		usedItems[bestLoc] = true
+		return bestLink, bestItem, bestDiff
+	end
+end
+
+local function renderEmptyGear(container)
+
+	local panelBlank = AceGUI:Create("AmrUiPanel")
+	panelBlank:SetLayout("None")
+	panelBlank:SetBackgroundColor(Amr.Colors.Black, 0.4)
+	panelBlank:SetPoint("TOPLEFT", container.content, "TOPLEFT", 6, 0)
+	panelBlank:SetPoint("BOTTOMRIGHT", container.content, "BOTTOMRIGHT")
+	container:AddChild(panelBlank)
+	
+	local lbl = AceGUI:Create("AmrUiLabel")
+	lbl:SetText(L.GearBlank)
+	lbl:SetWidth(700)
+	lbl:SetJustifyH("MIDDLE")
+	lbl:SetFont(Amr.CreateFont("Italic", 16, Amr.Colors.TextTan))		
+	lbl:SetPoint("BOTTOM", panelBlank.content, "CENTER", 0, 20)
+	panelBlank:AddChild(lbl)
+	
+	local lbl2 = AceGUI:Create("AmrUiLabel")
+	lbl2:SetText(L.GearBlank2)
+	lbl2:SetWidth(700)
+	lbl2:SetJustifyH("MIDDLE")
+	lbl2:SetFont(Amr.CreateFont("Italic", 16, Amr.Colors.TextTan))		
+	lbl2:SetPoint("TOP", lbl.frame, "CENTER", 0, -20)
+	panelBlank:AddChild(lbl2)
+end
+
+local function renderGear(spec, container)
+
+	local player = Amr:ExportCharacter()
+	local gear = Amr.db.char.GearSets[spec]
+	local equipped = player.Equipped[player.ActiveSpec]
+		
+	if not gear then
+		-- no gear has been imported for this spec so show a message
+		renderEmptyGear(container)
+	else
+		local panelGear = AceGUI:Create("AmrUiPanel")
+		panelGear:SetLayout("None")
+		panelGear:SetBackgroundColor(Amr.Colors.Black, 0.3)
+		panelGear:SetPoint("TOPLEFT", container.content, "TOPLEFT", 6, 0)
+		panelGear:SetPoint("BOTTOMRIGHT", container.content, "BOTTOMRIGHT", -300, 0)
+		container:AddChild(panelGear)
+		
+		local panelMods = AceGUI:Create("AmrUiPanel")
+		panelMods:SetLayout("None")
+		panelMods:SetPoint("TOPLEFT", panelGear.frame, "TOPRIGHT", 15, 0)
+		panelMods:SetPoint("BOTTOMRIGHT", container.content, "BOTTOMRIGHT")
+		panelMods:SetBackgroundColor(Amr.Colors.Black, 0.3)
+		container:AddChild(panelMods)
+		
+		-- spec icon
+		local icon = AceGUI:Create("AmrUiIcon")	
+		icon:SetIconBorderColor(Amr.Colors.Classes[player.Class])
+		icon:SetWidth(48)
+		icon:SetHeight(48)
+		
+		local iconSpec
+		if player.SubSpecs[spec] then
+			iconSpec = player.SubSpecs[spec]
+		else
+			iconSpec = player.Specs[spec]
+		end
+
+		icon:SetIcon("Interface\\Icons\\" .. Amr.SpecIcons[iconSpec])
+		icon:SetPoint("TOPLEFT", panelGear.content, "TOPLEFT", 10, -10)
+		panelGear:AddChild(icon)
+		
+		local btnEquip = AceGUI:Create("AmrUiButton")
+		btnEquip:SetText(L.GearButtonEquip(spec))
+		btnEquip:SetBackgroundColor(Amr.Colors.Green)
+		btnEquip:SetFont(Amr.CreateFont("Regular", 14, Amr.Colors.White))
+		btnEquip:SetWidth(300)
+		btnEquip:SetHeight(26)
+		btnEquip:SetPoint("LEFT", icon.frame, "RIGHT", 40, 0)
+		btnEquip:SetPoint("RIGHT", panelGear.content, "RIGHT", -40, 0)
+		btnEquip:SetCallback("OnClick", function(widget)
+			Amr:EquipGearSet(spec)			
+		end)
+		panelGear:AddChild(btnEquip)
+		
+		local btnShop = AceGUI:Create("AmrUiButton")
+		btnShop:SetText(L.GearButtonShop)
+		btnShop:SetBackgroundColor(Amr.Colors.Blue)
+		btnShop:SetFont(Amr.CreateFont("Regular", 14, Amr.Colors.White))
+		btnShop:SetWidth(300)
+		btnShop:SetHeight(26)
+		btnShop:SetPoint("LEFT", btnEquip.frame, "RIGHT", 75, 0)
+		btnShop:SetPoint("RIGHT", panelMods.content, "RIGHT", -20, 0)
+		btnShop:SetCallback("OnClick", function(widget) Amr:ShowShopWindow() end)
+		panelMods:AddChild(btnShop)
+		
+		-- each physical item can only be used once, this tracks ones we have already used
+		local usedItems = {}
+		
+		-- gear list
+		local prevElem = icon
+		for slotNum = 1, #Amr.SlotIds do
+			local slotId = Amr.SlotIds[slotNum]
+			
+			local equippedItemLink = equipped and equipped[slotId] or nil
+			local equippedItem = Amr.ParseItemLink(equippedItemLink)
+			local optimalItem = gear[slotId]			
+			local optimalItemLink = Amr.CreateItemLink(optimalItem)
+			
+			-- see if item is currently equipped, is false if don't have any item for that slot (e.g. OH for a 2-hander)
+			local isEquipped = false			
+			if equippedItem and optimalItem and Amr.GetItemUniqueId(equippedItem) == Amr.GetItemUniqueId(optimalItem) then
+				isEquipped = true
+			end
+			
+			-- find the item in the player's inventory that best matches what the optimization wants to use
+			local matchItemLink, matchItem = Amr:FindMatchingItem(optimalItem, player, usedItems)
+			
+			-- slot label
+			local lbl = AceGUI:Create("AmrUiLabel")
+			lbl:SetText(Amr.SlotDisplayText[slotId])
+			lbl:SetWidth(85)
+			lbl:SetFont(Amr.CreateFont("Regular", 14, Amr.Colors.White))		
+			lbl:SetPoint("TOPLEFT", prevElem.frame, "BOTTOMLEFT", 0, -12) 
+			panelGear:AddChild(lbl)
+			prevElem = lbl
+			
+			-- ilvl label
+			local lblIlvl = AceGUI:Create("AmrUiLabel")
+			lblIlvl:SetWidth(45)
+			lblIlvl:SetFont(Amr.CreateFont("Italic", 14, Amr.Colors.TextTan))		
+			lblIlvl:SetPoint("TOPLEFT", lbl.frame, "TOPRIGHT", 0, 0) 
+			panelGear:AddChild(lblIlvl)
+			
+			-- equipped label
+			local lblEquipped = AceGUI:Create("AmrUiLabel")
+			lblEquipped:SetWidth(20)
+			lblEquipped:SetFont(Amr.CreateFont("Regular", 14, Amr.Colors.White))
+			lblEquipped:SetPoint("TOPLEFT", lblIlvl.frame, "TOPRIGHT", 0, 0) 
+			lblEquipped:SetText(isEquipped and "E" or "")
+			panelGear:AddChild(lblEquipped)
+			
+			-- item name/link label
+			local lblItem = AceGUI:Create("AmrUiLabel")
+			lblItem:SetWordWrap(false)
+			lblItem:SetWidth(345)
+			lblItem:SetFont(Amr.CreateFont(isEquipped and "Regular" or "Bold", isEquipped and 14 or 15, Amr.Colors.White))		
+			lblItem:SetPoint("TOPLEFT", lblEquipped.frame, "TOPRIGHT", 0, 0) 
+			panelGear:AddChild(lblItem)
+			
+			-- fill the name/ilvl labels, which may require asynchronous loading of item information
+			if optimalItemLink then
+				Amr.GetItemInfo(optimalItemLink, function(obj, name, link, quality, iLevel)					
+					-- set item name, tooltip, and ilvl
+					obj.nameLabel:SetText(link:gsub("%[", ""):gsub("%]", ""))
+					Amr:SetItemTooltip(obj.nameLabel, link)
+					obj.ilvlLabel:SetText(iLevel)					
+				end, { ilvlLabel = lblIlvl, nameLabel = lblItem })
+			end
+						
+			-- modifications
+			if optimalItem then
+				local itemInfo = Amr.db.char.ExtraItemData[spec][optimalItem.id]
+
+				-- gems
+				if itemInfo and itemInfo.socketColors then
+					for i = 1, #itemInfo.socketColors do
+						local g = optimalItem.gemIds[i]
+						local isGemEquipped = g ~= 0 and matchItem and matchItem.gemIds and matchItem.gemIds[i] == g
+						
+						-- highlight for gem that doesn't match
+						local socketBorder = AceGUI:Create("AmrUiPanel")
+						socketBorder:SetLayout("None")
+						socketBorder:SetBackgroundColor(Amr.Colors.Black, isGemEquipped and 0 or 1)
+						socketBorder:SetWidth(26)
+						socketBorder:SetHeight(26)
+						socketBorder:SetPoint("LEFT", lblItem.frame, "RIGHT", 30, 0)
+						if isGemEquipped then
+							socketBorder:SetAlpha(0.3)
+						end
+						panelMods:AddChild(socketBorder)
+						
+						local socketBg = AceGUI:Create("AmrUiIcon")
+						socketBg:SetLayout("None")
+						socketBg:SetBorderWidth(2)
+						socketBg:SetIconBorderColor(Amr.Colors.Green, isGemEquipped and 0 or 1)
+						socketBg:SetWidth(24)
+						socketBg:SetHeight(24)
+						socketBg:SetPoint("TOPLEFT", socketBorder.content, "TOPLEFT", 1, -1)
+						socketBorder:AddChild(socketBg)
+						
+						local socketIcon = AceGUI:Create("AmrUiIcon")
+						socketIcon:SetBorderWidth(1)
+						socketIcon:SetIconBorderColor(Amr.Colors.White)
+						socketIcon:SetWidth(18)
+						socketIcon:SetHeight(18)
+						socketIcon:SetPoint("CENTER", socketBg.content, "CENTER")
+						socketBg:AddChild(socketIcon)
+						
+						-- get icon for optimized gem
+						if g ~= 0 then
+							local gemInfo = Amr.db.char.ExtraGemData[spec][g]
+							if gemInfo then
+								Amr.GetItemInfo(gemInfo.id, function(obj, name, link, quality, iLevel, reqLevel, class, subclass, maxStack, equipSlot, texture)					
+									-- set icon and a tooltip
+									obj:SetIcon(texture)
+									Amr:SetItemTooltip(obj, link)
+								end, socketIcon)
+							end
+						end
+					end
+				end
+				
+				-- enchant
+				if optimalItem.enchantId and optimalItem.enchantId ~= 0 then
+					local isEnchantEquipped = matchItem and matchItem.enchantId and matchItem.enchantId == optimalItem.enchantId
+					
+					local lblEnchant = AceGUI:Create("AmrUiLabel")
+					lblEnchant:SetWordWrap(false)
+					lblEnchant:SetWidth(170)
+					lblEnchant:SetFont(Amr.CreateFont(isEnchantEquipped and "Regular" or "Bold", 14, isEnchantEquipped and Amr.Colors.TextGray or Amr.Colors.White))
+					lblEnchant:SetPoint("TOPLEFT", lblItem.frame, "TOPRIGHT", 130, 0)
+					
+					local enchInfo = Amr.db.char.ExtraEnchantData[spec][optimalItem.enchantId]
+					if enchInfo then
+						lblEnchant:SetText(enchInfo.text)
+						
+						Amr.GetItemInfo(enchInfo.itemId, function(obj, name, link)					
+							Amr:SetItemTooltip(obj, link)
+						end, lblEnchant)						
+						--Amr:SetSpellTooltip(lblEnchant, enchInfo.spellId)
+					end
+					panelMods:AddChild(lblEnchant)
+				end
+			end
+			
+			prevElem = lbl
+		end
+	end
+end
+
+local function onGearTabSelected(container, event, group)
+	container:ReleaseChildren()
+	_activeTab = group
+	renderGear(tonumber(group), container)
+end
+
+local function onImportClick(widget)
+	Amr:ShowImportWindow()
+end
+
+-- renders the main UI for the Gear tab
+function Amr:RenderTabGear(container)
+
+	local btnImport = AceGUI:Create("AmrUiButton")
+	btnImport:SetText(L.GearButtonImportText)
+	btnImport:SetBackgroundColor(Amr.Colors.Orange)
+	btnImport:SetFont(Amr.CreateFont("Bold", 16, Amr.Colors.White))
+	btnImport:SetWidth(120)
+	btnImport:SetHeight(26)
+	btnImport:SetPoint("TOPLEFT", container.content, "TOPLEFT", 0, -81)
+	btnImport:SetCallback("OnClick", onImportClick)
+	container:AddChild(btnImport)	
+	
+	local lbl = AceGUI:Create("AmrUiLabel")
+	lbl:SetText(L.GearImportNote)
+	lbl:SetWidth(100)
+	lbl:SetFont(Amr.CreateFont("Italic", 12, Amr.Colors.TextTan))
+	lbl:SetJustifyH("MIDDLE")
+	lbl:SetPoint("TOP", btnImport.frame, "BOTTOM", 0, -5)
+	container:AddChild(lbl)
+	
+	local lbl2 = AceGUI:Create("AmrUiLabel")
+	lbl2:SetText(L.GearTipTitle)
+	lbl2:SetWidth(140)
+	lbl2:SetFont(Amr.CreateFont("Italic", 20, Amr.Colors.Text))
+	lbl2:SetJustifyH("MIDDLE")
+	lbl2:SetPoint("TOP", lbl.frame, "BOTTOM", 0, -50)
+	container:AddChild(lbl2)
+	
+	lbl = AceGUI:Create("AmrUiLabel")
+	lbl:SetText(L.GearTipText)
+	lbl:SetWidth(140)
+	lbl:SetFont(Amr.CreateFont("Italic", 12, Amr.Colors.Text))
+	lbl:SetJustifyH("MIDDLE")
+	lbl:SetPoint("TOP", lbl2.frame, "BOTTOM", 0, -5)
+	container:AddChild(lbl)
+	
+	lbl2 = AceGUI:Create("AmrUiLabel")
+	lbl2:SetText(L.GearTipCommands)
+	lbl2:SetWidth(130)
+	lbl2:SetFont(Amr.CreateFont("Italic", 12, Amr.Colors.Text))
+	lbl2:SetPoint("TOP", lbl.frame, "BOTTOM", 10, -5)
+	container:AddChild(lbl2)
+	
+	--[[
+	local btnClean = AceGUI:Create("AmrUiButton")
+	btnClean:SetText(L.GearButtonCleanText)
+	btnClean:SetBackgroundColor(Amr.Colors.Orange)
+	btnClean:SetFont(Amr.CreateFont("Bold", 16, Amr.Colors.White))
+	btnClean:SetWidth(120)
+	btnClean:SetHeight(26)
+	btnClean:SetPoint("BOTTOMLEFT", container.content, "BOTTOMLEFT", 0, 5)
+	btnClean:SetCallback("OnClick", function(widget) Amr:CleanBags() end)
+	container:AddChild(btnClean)	
+	]]
+	
+	local t =  AceGUI:Create("AmrUiTabGroup")
+	t:SetLayout("None")
+	t:SetTabs({
+		{text=L.GearTabPrimary, value="1", style="bold"}, 
+		{text=L.GearTabSecondary, value="2", style="bold"}
+	})
+	t:SetCallback("OnGroupSelected", onGearTabSelected)
+	t:SetPoint("TOPLEFT", container.content, "TOPLEFT", 144, -30)
+	t:SetPoint("BOTTOMRIGHT", container.content, "BOTTOMRIGHT")
+	container:AddChild(t)	
+	_gearTabs = t;
+	
+	if not _activeTab then
+		_activeTab = tostring(GetActiveSpecGroup())
+	end
+	
+	t:SelectTab(_activeTab)
+end
+
+-- do cleanup when the gear tab is released
+function Amr:ReleaseTabGear()
+	_gearTabs = nil
+end
+
+-- show and update the gear tab for the specified spec
+function Amr:ShowGearTab(spec)
+	if not _gearTabs then return end
+	
+	_activeTab = tostring(spec)
+	_gearTabs:SelectTab(_activeTab)
+end
+
+-- refresh display of the current gear tab
+function Amr:RefreshGearTab()
+	if not _gearTabs then return end
+	_gearTabs:SelectTab(_activeTab)
+end
+
+
+------------------------------------------------------------------------------------------------
+-- Gear Set Management
+------------------------------------------------------------------------------------------------
+local _waitingForSpec = 0
+local _waitingForItemLock = nil
+local _pendingEquip = nil
+
+-- scan a bag for the best matching item
+local function scanBagForItem(item, bagId, bestItem, bestDiff, bestLink)
+	local numSlots = GetContainerNumSlots(bagId)
+	for slotId = 1, numSlots do
+		local _, _, _, _, _, _, itemLink = GetContainerItemInfo(bagId, slotId)
+        -- we skip any stackable item, as far as we know, there is no equippable gear that can be stacked
+		if itemLink then
+			local bagItem = Amr.ParseItemLink(itemLink)
+			if bagItem ~= nil then
+				local diff = countItemDifferences(item, bagItem)
+				if diff < bestDiff then
+					bestItem = { bag = bagId, slot = slotId }
+					bestDiff = diff
+					bestLink = itemLink
+				end
+            end
+		end
+	end
+	return bestItem, bestDiff, bestLink
+end
+
+-- find the first empty slot in the player's backpack+bags
+local function findFirstEmptyBagSlot()
+	
+	local bagIds = {}
+	table.insert(bagIds, BACKPACK_CONTAINER)
+	for bagId = 1, NUM_BAG_SLOTS do
+		table.insert(bagIds, bagId)
+	end
+	
+	for i, bagId in ipairs(bagIds) do
+		local numSlots = GetContainerNumSlots(bagId)
+		for slotId = 1, numSlots do
+			local _, _, _, _, _, _, itemLink = GetContainerItemInfo(bagId, slotId)
+			if not itemLink then
+				return bagId, slotId
+			end
+		end
+	end
+	
+	return nil, nil
+end
+
+local function finishEquipGearSet()
+	if not _pendingEquip then return end
+	
+	_pendingEquip.tries = _pendingEquip.tries + 1
+	if _pendingEquip.tries > 16 then
+		_pendingEquip = nil
+	else
+		-- start over again, trying any items that could not be equipped in the previous pass (unique constraints)
+		Amr:EquipGearSet(_pendingEquip.spec)
+	end
+end
+
+-- equip the next slot in a pending equip
+local function tryEquipNextItem()
+	if not _pendingEquip then return end
+	
+	local item = _pendingEquip.itemsToEquip[_pendingEquip.nextSlot]
+	
+	local bestItem = nil
+	local bestLink = nil
+	local bestDiff = 1000
+	
+	-- find the best matching item
+	
+	-- equipped items
+	for slotNum = 1, #Amr.SlotIds do
+		local slotId = Amr.SlotIds[slotNum]
+		local itemLink = GetInventoryItemLink("player", slotId)
+		if itemLink then
+			local invItem = Amr.ParseItemLink(itemLink)
+			if invItem ~= nil then
+				local diff = countItemDifferences(item, invItem)
+				if diff < bestDiff then
+					bestItem = { slot = slotId }
+					bestDiff = diff
+					bestLink = itemLink
+				end
+			end
+		end
+	end
+	
+	-- inventory
+	bestItem, bestDiff, bestLink = scanBagForItem(item, BACKPACK_CONTAINER, bestItem, bestDiff, bestLink)
+	for bagId = 1, NUM_BAG_SLOTS do
+		bestItem, bestDiff, bestLink = scanBagForItem(item, bagId, bestItem, bestDiff, bestLink)
+	end
+	
+	-- bank
+	bestItem, bestDiff = scanBagForItem(item, BANK_CONTAINER, bestItem, bestDiff, bestLink)
+	for bagId = NUM_BAG_SLOTS + 1, NUM_BAG_SLOTS + NUM_BANKBAGSLOTS do
+		bestItem, bestDiff = scanBagForItem(item, bagId, bestItem, bestDiff, bestLink)
+	end
+	
+	ClearCursor()
+	
+	if not bestItem then
+		-- stop if we can't find an item
+		Amr:Print(L.GearEquipErrorNotFound)
+		Amr:Print(L.GearEquipErrorNotFound2)
+		_pendingEquip = nil
+		return
+		
+	elseif bestItem and bestItem.bag and bestItem.bag >= NUM_BAG_SLOTS + 1 and bestItem.bag <= NUM_BAG_SLOTS + NUM_BANKBAGSLOTS then
+		-- find first empty bag slot
+		local invBag, invSlot = findFirstEmptyBagSlot()
+		if not invBag then
+			-- stop if bags are too full
+			Amr:Print(L.GearEquipErrorBagFull)
+			_pendingEquip = nil
+			return
+		end
+
+		-- move from bank to bag
+		PickupContainerItem(bestItem.bag, bestItem.slot)
+		PickupContainerItem(invBag, invSlot)
+
+		-- set flag so that when we clear cursor and release the item lock, we can respond to the event and continue
+		_waitingForItemLock = {
+			bagId = invBag,
+			slotId = invSlot
+		}
+		
+		ClearCursor()
+		
+		-- now we need to wait for game event to continue and try this item again after it is in our bag
+		return
+	else
+		if not Amr:IsSoulbound(bestItem.bag, bestItem.slot) then
+			-- if an item is not soulbound, then warn the user and quit
+			Amr:Print(L.GearEquipErrorSoulbound(bestLink))
+			_pendingEquip = nil
+			return
+		else
+			local slotId = _pendingEquip.nextSlot
+			
+			-- an item in the player's bags or already equipped, equip it
+			_pendingEquip.bag = bestItem.bag
+			_pendingEquip.slot = bestItem.slot
+			_pendingEquip.destSlot = slotId
+			
+			if bestItem.bag then
+				PickupContainerItem(bestItem.bag, bestItem.slot)
+			else
+				PickupInventoryItem(bestItem.slot)
+			end
+			PickupInventoryItem(slotId)
+			ClearCursor()
+		end
+	end
+	
+end
+
+local function onItemUnlocked(bagId, slotId)
+
+	if _waitingForItemLock then
+		-- waiting on a move from bank to bags to complete, just continue as normal afterwards
+		if bagId == _waitingForItemLock.bagId and slotId == _waitingForItemLock.slotId then
+			_waitingForItemLock = nil
+			tryEquipNextItem()
+		end
+		
+	elseif _pendingEquip and _pendingEquip.destSlot then
+		-- waiting on an item swap to complete successfully so that we can go on to the next item
+		
+		-- inventory slot we're swapping to is still locked, can't continue yet
+		if IsInventoryItemLocked(_pendingEquip.destSlot) then return end
+
+		if _pendingEquip.bag then
+			local _, _, locked = GetContainerItemInfo(_pendingEquip.bag, _pendingEquip.slot)
+			-- the bag slot we're swapping from is still locked, can't continue yet
+			if locked then return end
+		else
+			-- inventory slot we're swapping from is still locked, can't continue yet
+			if IsInventoryItemLocked(_pendingEquip.slot) then return end
+		end
+		
+		-- move on to the next item, this item is done
+		_pendingEquip.itemsToEquip[_pendingEquip.destSlot] = nil
+		_pendingEquip.destSlot = nil
+		_pendingEquip.bag = nil
+		_pendingEquip.slot = nil
+		
+		_pendingEquip.remaining = _pendingEquip.remaining - 1
+		if _pendingEquip.remaining > 0 then
+			for slotId, item in pairs(_pendingEquip.itemsToEquip) do
+				_pendingEquip.nextSlot = slotId
+				break
+			end
+			tryEquipNextItem()
+		else
+			finishEquipGearSet()
+		end
+		
+	end
+end
+
+local function startEquipGearSet(spec)
+
+	local gear = Amr.db.char.GearSets[spec]
+	if not gear then 
+		Amr:Print(L.GearEquipErrorEmpty)
+		return
+	end
+	
+	local player = Amr:ExportCharacter()
+
+	local itemsToEquip = {}
+	local remaining = 0
+	local usedItems = {}
+	local firstSlot = nil
+	
+	-- check for items that need to be equipped
+	for slotNum = 1, #Amr.SlotIds do
+		local slotId = Amr.SlotIds[slotNum]
+		
+		local old = player.Equipped[spec][slotId]
+		old = Amr.ParseItemLink(old)
+		
+		local new = gear[slotId]
+		
+		local diff = countItemDifferences(old, new)
+		if diff < 1000 then
+			-- same item, see if inventory has one that is closer (e.g. a duplicate item with correct enchants/gems)
+			local bestLink, bestItem, bestDiff = Amr:FindMatchingItem(new, player, usedItems)
+			if bestDiff and bestDiff < diff then
+				itemsToEquip[slotId] = new
+				remaining = remaining + 1
+			end
+		else
+			itemsToEquip[slotId] = new
+			remaining = remaining + 1
+		end
+	end
+
+	if remaining > 0 then
+		_pendingEquip = {
+			tries = _pendingEquip and _pendingEquip.spec == spec and _pendingEquip.tries or 0,
+			spec = spec,
+			itemsToEquip = itemsToEquip,
+			remaining = remaining,
+			nextSlot = firstSlot
+		}
+
+		-- starting item
+		for slotId, item in pairs(_pendingEquip.itemsToEquip) do
+			_pendingEquip.nextSlot = slotId
+			break
+		end
+		
+		tryEquipNextItem()
+	else
+		_pendingEquip = nil
+	end
+end
+
+local function onActiveTalentGroupChanged()
+	local auto = Amr.db.profile.options.autoGear
+	local currentSpec = GetActiveSpecGroup()
+	
+	if currentSpec == _waitingForSpec or auto then
+		-- spec is what we want, now equip the gear
+		startEquipGearSet(currentSpec)
+	end
+	
+	_waitingForSpec = 0
+end
+
+-- activate the specified spec and then equip the saved gear set for either primary (1) or secondary (2) spec
+function Amr:EquipGearSet(spec)
+	
+	-- if no argument, then toggle spec
+	if not spec then
+		spec = GetActiveSpecGroup() == 1 and 2 or 1
+	end
+	
+	-- allow some flexibility in the arguments
+	if spec == "primary" or spec == "Primary" then spec = 1 end
+	if spec == "secondary" or spec == "Secondary" then spec = 2 end
+	if spec == "1" or spec == "2" then spec = tonumber(spec) end
+	
+	-- only spec 1 or 2 are valid
+	if spec ~= 1 and spec ~= 2 then return end
+	
+	if UnitAffectingCombat("player") then
+		Amr:Print(L.GearEquipErrorCombat)
+		return
+	end
+	
+	_waitingForSpec = spec
+	
+	local currentSpec = GetActiveSpecGroup()
+	if currentSpec ~= spec then
+		SetActiveSpecGroup(spec)
+	else
+		onActiveTalentGroupChanged()
+	end
+end
+
+-- moves any gear in bags to the bank if not part of main or off spec gear set
+function Amr:CleanBags()
+	-- TODO: implement
+end
+
+function Amr:InitializeGear()
+	Amr:AddEventHandler("ACTIVE_TALENT_GROUP_CHANGED", onActiveTalentGroupChanged)
+
+	Amr:AddEventHandler("UNIT_INVENTORY_CHANGED", function(unitID)
+		if unitID and unitID ~= "player" then return end
+		Amr:RefreshGearTab()
+	end)
+
+	Amr:AddEventHandler("ITEM_UNLOCKED", onItemUnlocked)
+end
\ No newline at end of file