Mercurial > wow > askmrrobot
comparison Gear.lua @ 57:01b63b8ed811 v21
total rewrite to version 21
| author | yellowfive |
|---|---|
| date | Fri, 05 Jun 2015 11:05:15 -0700 |
| parents | |
| children | ee701ce45354 |
comparison
equal
deleted
inserted
replaced
| 56:75431c084aa0 | 57:01b63b8ed811 |
|---|---|
| 1 local Amr = LibStub("AceAddon-3.0"):GetAddon("AskMrRobot") | |
| 2 local L = LibStub("AceLocale-3.0"):GetLocale("AskMrRobot", true) | |
| 3 local AceGUI = LibStub("AceGUI-3.0") | |
| 4 | |
| 5 local _gearTabs | |
| 6 local _activeTab | |
| 7 | |
| 8 -- Returns a number indicating how different two items are (0 means the same, higher means more different) | |
| 9 local function countItemDifferences(item1, item2) | |
| 10 if item1 == nil and item2 == nil then return 0 end | |
| 11 | |
| 12 -- different items (id + bonus ids + suffix, constitutes a different physical drop) | |
| 13 if Amr.GetItemUniqueId(item1, true) ~= Amr.GetItemUniqueId(item2, true) then | |
| 14 return 1000 | |
| 15 end | |
| 16 | |
| 17 -- different upgrade levels of the same item (only for older gear, player has control over upgrade level) | |
| 18 if item1.upgradeId ~= item2.upgradeId then | |
| 19 return 100 | |
| 20 end | |
| 21 | |
| 22 -- different gems | |
| 23 local gemDiffs = 0 | |
| 24 for i = 1, 3 do | |
| 25 if item1.gemIds[i] ~= item2.gemIds[i] then | |
| 26 gemDiffs = gemDiffs + 1 | |
| 27 end | |
| 28 end | |
| 29 | |
| 30 -- different enchants | |
| 31 local enchantDiff = 0 | |
| 32 if item1.enchantId ~= item2.enchantId then | |
| 33 enchantDiff = 1 | |
| 34 end | |
| 35 | |
| 36 return gemDiffs + enchantDiff | |
| 37 end | |
| 38 | |
| 39 -- given a table of items (keyed or indexed doesn't matter) find closest match to item, or nil if none are a match | |
| 40 local function findMatchingItemFromTable(item, list, bestLink, bestItem, bestDiff, bestLoc, usedItems, tableType) | |
| 41 if not list then return nil end | |
| 42 | |
| 43 for k,v in pairs(list) do | |
| 44 local listItem = Amr.ParseItemLink(v) | |
| 45 if listItem then | |
| 46 local diff = countItemDifferences(item, listItem) | |
| 47 if diff < bestDiff then | |
| 48 -- each physical item can only be used once, the usedItems table has items we can't use in this search | |
| 49 local key = string.format("%s_%s", tableType, k) | |
| 50 if not usedItems[key] then | |
| 51 bestLink = v | |
| 52 bestItem = listItem | |
| 53 bestDiff = diff | |
| 54 bestLoc = string.format("%s_%s", tableType, k) | |
| 55 end | |
| 56 end | |
| 57 if diff == 0 then break end | |
| 58 end | |
| 59 end | |
| 60 | |
| 61 return bestLink, bestItem, bestDiff, bestLoc | |
| 62 end | |
| 63 | |
| 64 -- search the player's equipped gear, bag, bank, and void storage for an item that best matches the specified item | |
| 65 function Amr:FindMatchingItem(item, player, usedItems) | |
| 66 if not item then return nil end | |
| 67 | |
| 68 local equipped = player.Equipped and player.Equipped[player.ActiveSpec] or nil | |
| 69 local bestLink, bestItem, bestDiff, bestLoc = findMatchingItemFromTable(item, equipped, nil, nil, 1000, nil, usedItems, "equip") | |
| 70 bestLink, bestItem, bestDiff, bestLoc = findMatchingItemFromTable(item, player.BagItems, bestLink, bestItem, bestDiff, bestLoc, usedItems, "bag") | |
| 71 bestLink, bestItem, bestDiff, bestLoc = findMatchingItemFromTable(item, player.BankItems, bestLink, bestItem, bestDiff, bestLoc, usedItems, "bank") | |
| 72 bestLink, bestItem, bestDiff, bestLoc = findMatchingItemFromTable(item, player.VoidItems, bestLink, bestItem, bestDiff, bestLoc, usedItems, "void") | |
| 73 | |
| 74 if bestDiff >= 1000 then | |
| 75 return nil, nil, 1000 | |
| 76 else | |
| 77 usedItems[bestLoc] = true | |
| 78 return bestLink, bestItem, bestDiff | |
| 79 end | |
| 80 end | |
| 81 | |
| 82 local function renderEmptyGear(container) | |
| 83 | |
| 84 local panelBlank = AceGUI:Create("AmrUiPanel") | |
| 85 panelBlank:SetLayout("None") | |
| 86 panelBlank:SetBackgroundColor(Amr.Colors.Black, 0.4) | |
| 87 panelBlank:SetPoint("TOPLEFT", container.content, "TOPLEFT", 6, 0) | |
| 88 panelBlank:SetPoint("BOTTOMRIGHT", container.content, "BOTTOMRIGHT") | |
| 89 container:AddChild(panelBlank) | |
| 90 | |
| 91 local lbl = AceGUI:Create("AmrUiLabel") | |
| 92 lbl:SetText(L.GearBlank) | |
| 93 lbl:SetWidth(700) | |
| 94 lbl:SetJustifyH("MIDDLE") | |
| 95 lbl:SetFont(Amr.CreateFont("Italic", 16, Amr.Colors.TextTan)) | |
| 96 lbl:SetPoint("BOTTOM", panelBlank.content, "CENTER", 0, 20) | |
| 97 panelBlank:AddChild(lbl) | |
| 98 | |
| 99 local lbl2 = AceGUI:Create("AmrUiLabel") | |
| 100 lbl2:SetText(L.GearBlank2) | |
| 101 lbl2:SetWidth(700) | |
| 102 lbl2:SetJustifyH("MIDDLE") | |
| 103 lbl2:SetFont(Amr.CreateFont("Italic", 16, Amr.Colors.TextTan)) | |
| 104 lbl2:SetPoint("TOP", lbl.frame, "CENTER", 0, -20) | |
| 105 panelBlank:AddChild(lbl2) | |
| 106 end | |
| 107 | |
| 108 local function renderGear(spec, container) | |
| 109 | |
| 110 local player = Amr:ExportCharacter() | |
| 111 local gear = Amr.db.char.GearSets[spec] | |
| 112 local equipped = player.Equipped[player.ActiveSpec] | |
| 113 | |
| 114 if not gear then | |
| 115 -- no gear has been imported for this spec so show a message | |
| 116 renderEmptyGear(container) | |
| 117 else | |
| 118 local panelGear = AceGUI:Create("AmrUiPanel") | |
| 119 panelGear:SetLayout("None") | |
| 120 panelGear:SetBackgroundColor(Amr.Colors.Black, 0.3) | |
| 121 panelGear:SetPoint("TOPLEFT", container.content, "TOPLEFT", 6, 0) | |
| 122 panelGear:SetPoint("BOTTOMRIGHT", container.content, "BOTTOMRIGHT", -300, 0) | |
| 123 container:AddChild(panelGear) | |
| 124 | |
| 125 local panelMods = AceGUI:Create("AmrUiPanel") | |
| 126 panelMods:SetLayout("None") | |
| 127 panelMods:SetPoint("TOPLEFT", panelGear.frame, "TOPRIGHT", 15, 0) | |
| 128 panelMods:SetPoint("BOTTOMRIGHT", container.content, "BOTTOMRIGHT") | |
| 129 panelMods:SetBackgroundColor(Amr.Colors.Black, 0.3) | |
| 130 container:AddChild(panelMods) | |
| 131 | |
| 132 -- spec icon | |
| 133 local icon = AceGUI:Create("AmrUiIcon") | |
| 134 icon:SetIconBorderColor(Amr.Colors.Classes[player.Class]) | |
| 135 icon:SetWidth(48) | |
| 136 icon:SetHeight(48) | |
| 137 | |
| 138 local iconSpec | |
| 139 if player.SubSpecs[spec] then | |
| 140 iconSpec = player.SubSpecs[spec] | |
| 141 else | |
| 142 iconSpec = player.Specs[spec] | |
| 143 end | |
| 144 | |
| 145 icon:SetIcon("Interface\\Icons\\" .. Amr.SpecIcons[iconSpec]) | |
| 146 icon:SetPoint("TOPLEFT", panelGear.content, "TOPLEFT", 10, -10) | |
| 147 panelGear:AddChild(icon) | |
| 148 | |
| 149 local btnEquip = AceGUI:Create("AmrUiButton") | |
| 150 btnEquip:SetText(L.GearButtonEquip(spec)) | |
| 151 btnEquip:SetBackgroundColor(Amr.Colors.Green) | |
| 152 btnEquip:SetFont(Amr.CreateFont("Regular", 14, Amr.Colors.White)) | |
| 153 btnEquip:SetWidth(300) | |
| 154 btnEquip:SetHeight(26) | |
| 155 btnEquip:SetPoint("LEFT", icon.frame, "RIGHT", 40, 0) | |
| 156 btnEquip:SetPoint("RIGHT", panelGear.content, "RIGHT", -40, 0) | |
| 157 btnEquip:SetCallback("OnClick", function(widget) | |
| 158 Amr:EquipGearSet(spec) | |
| 159 end) | |
| 160 panelGear:AddChild(btnEquip) | |
| 161 | |
| 162 local btnShop = AceGUI:Create("AmrUiButton") | |
| 163 btnShop:SetText(L.GearButtonShop) | |
| 164 btnShop:SetBackgroundColor(Amr.Colors.Blue) | |
| 165 btnShop:SetFont(Amr.CreateFont("Regular", 14, Amr.Colors.White)) | |
| 166 btnShop:SetWidth(300) | |
| 167 btnShop:SetHeight(26) | |
| 168 btnShop:SetPoint("LEFT", btnEquip.frame, "RIGHT", 75, 0) | |
| 169 btnShop:SetPoint("RIGHT", panelMods.content, "RIGHT", -20, 0) | |
| 170 btnShop:SetCallback("OnClick", function(widget) Amr:ShowShopWindow() end) | |
| 171 panelMods:AddChild(btnShop) | |
| 172 | |
| 173 -- each physical item can only be used once, this tracks ones we have already used | |
| 174 local usedItems = {} | |
| 175 | |
| 176 -- gear list | |
| 177 local prevElem = icon | |
| 178 for slotNum = 1, #Amr.SlotIds do | |
| 179 local slotId = Amr.SlotIds[slotNum] | |
| 180 | |
| 181 local equippedItemLink = equipped and equipped[slotId] or nil | |
| 182 local equippedItem = Amr.ParseItemLink(equippedItemLink) | |
| 183 local optimalItem = gear[slotId] | |
| 184 local optimalItemLink = Amr.CreateItemLink(optimalItem) | |
| 185 | |
| 186 -- see if item is currently equipped, is false if don't have any item for that slot (e.g. OH for a 2-hander) | |
| 187 local isEquipped = false | |
| 188 if equippedItem and optimalItem and Amr.GetItemUniqueId(equippedItem) == Amr.GetItemUniqueId(optimalItem) then | |
| 189 isEquipped = true | |
| 190 end | |
| 191 | |
| 192 -- find the item in the player's inventory that best matches what the optimization wants to use | |
| 193 local matchItemLink, matchItem = Amr:FindMatchingItem(optimalItem, player, usedItems) | |
| 194 | |
| 195 -- slot label | |
| 196 local lbl = AceGUI:Create("AmrUiLabel") | |
| 197 lbl:SetText(Amr.SlotDisplayText[slotId]) | |
| 198 lbl:SetWidth(85) | |
| 199 lbl:SetFont(Amr.CreateFont("Regular", 14, Amr.Colors.White)) | |
| 200 lbl:SetPoint("TOPLEFT", prevElem.frame, "BOTTOMLEFT", 0, -12) | |
| 201 panelGear:AddChild(lbl) | |
| 202 prevElem = lbl | |
| 203 | |
| 204 -- ilvl label | |
| 205 local lblIlvl = AceGUI:Create("AmrUiLabel") | |
| 206 lblIlvl:SetWidth(45) | |
| 207 lblIlvl:SetFont(Amr.CreateFont("Italic", 14, Amr.Colors.TextTan)) | |
| 208 lblIlvl:SetPoint("TOPLEFT", lbl.frame, "TOPRIGHT", 0, 0) | |
| 209 panelGear:AddChild(lblIlvl) | |
| 210 | |
| 211 -- equipped label | |
| 212 local lblEquipped = AceGUI:Create("AmrUiLabel") | |
| 213 lblEquipped:SetWidth(20) | |
| 214 lblEquipped:SetFont(Amr.CreateFont("Regular", 14, Amr.Colors.White)) | |
| 215 lblEquipped:SetPoint("TOPLEFT", lblIlvl.frame, "TOPRIGHT", 0, 0) | |
| 216 lblEquipped:SetText(isEquipped and "E" or "") | |
| 217 panelGear:AddChild(lblEquipped) | |
| 218 | |
| 219 -- item name/link label | |
| 220 local lblItem = AceGUI:Create("AmrUiLabel") | |
| 221 lblItem:SetWordWrap(false) | |
| 222 lblItem:SetWidth(345) | |
| 223 lblItem:SetFont(Amr.CreateFont(isEquipped and "Regular" or "Bold", isEquipped and 14 or 15, Amr.Colors.White)) | |
| 224 lblItem:SetPoint("TOPLEFT", lblEquipped.frame, "TOPRIGHT", 0, 0) | |
| 225 panelGear:AddChild(lblItem) | |
| 226 | |
| 227 -- fill the name/ilvl labels, which may require asynchronous loading of item information | |
| 228 if optimalItemLink then | |
| 229 Amr.GetItemInfo(optimalItemLink, function(obj, name, link, quality, iLevel) | |
| 230 -- set item name, tooltip, and ilvl | |
| 231 obj.nameLabel:SetText(link:gsub("%[", ""):gsub("%]", "")) | |
| 232 Amr:SetItemTooltip(obj.nameLabel, link) | |
| 233 obj.ilvlLabel:SetText(iLevel) | |
| 234 end, { ilvlLabel = lblIlvl, nameLabel = lblItem }) | |
| 235 end | |
| 236 | |
| 237 -- modifications | |
| 238 if optimalItem then | |
| 239 local itemInfo = Amr.db.char.ExtraItemData[spec][optimalItem.id] | |
| 240 | |
| 241 -- gems | |
| 242 if itemInfo and itemInfo.socketColors then | |
| 243 for i = 1, #itemInfo.socketColors do | |
| 244 local g = optimalItem.gemIds[i] | |
| 245 local isGemEquipped = g ~= 0 and matchItem and matchItem.gemIds and matchItem.gemIds[i] == g | |
| 246 | |
| 247 -- highlight for gem that doesn't match | |
| 248 local socketBorder = AceGUI:Create("AmrUiPanel") | |
| 249 socketBorder:SetLayout("None") | |
| 250 socketBorder:SetBackgroundColor(Amr.Colors.Black, isGemEquipped and 0 or 1) | |
| 251 socketBorder:SetWidth(26) | |
| 252 socketBorder:SetHeight(26) | |
| 253 socketBorder:SetPoint("LEFT", lblItem.frame, "RIGHT", 30, 0) | |
| 254 if isGemEquipped then | |
| 255 socketBorder:SetAlpha(0.3) | |
| 256 end | |
| 257 panelMods:AddChild(socketBorder) | |
| 258 | |
| 259 local socketBg = AceGUI:Create("AmrUiIcon") | |
| 260 socketBg:SetLayout("None") | |
| 261 socketBg:SetBorderWidth(2) | |
| 262 socketBg:SetIconBorderColor(Amr.Colors.Green, isGemEquipped and 0 or 1) | |
| 263 socketBg:SetWidth(24) | |
| 264 socketBg:SetHeight(24) | |
| 265 socketBg:SetPoint("TOPLEFT", socketBorder.content, "TOPLEFT", 1, -1) | |
| 266 socketBorder:AddChild(socketBg) | |
| 267 | |
| 268 local socketIcon = AceGUI:Create("AmrUiIcon") | |
| 269 socketIcon:SetBorderWidth(1) | |
| 270 socketIcon:SetIconBorderColor(Amr.Colors.White) | |
| 271 socketIcon:SetWidth(18) | |
| 272 socketIcon:SetHeight(18) | |
| 273 socketIcon:SetPoint("CENTER", socketBg.content, "CENTER") | |
| 274 socketBg:AddChild(socketIcon) | |
| 275 | |
| 276 -- get icon for optimized gem | |
| 277 if g ~= 0 then | |
| 278 local gemInfo = Amr.db.char.ExtraGemData[spec][g] | |
| 279 if gemInfo then | |
| 280 Amr.GetItemInfo(gemInfo.id, function(obj, name, link, quality, iLevel, reqLevel, class, subclass, maxStack, equipSlot, texture) | |
| 281 -- set icon and a tooltip | |
| 282 obj:SetIcon(texture) | |
| 283 Amr:SetItemTooltip(obj, link) | |
| 284 end, socketIcon) | |
| 285 end | |
| 286 end | |
| 287 end | |
| 288 end | |
| 289 | |
| 290 -- enchant | |
| 291 if optimalItem.enchantId and optimalItem.enchantId ~= 0 then | |
| 292 local isEnchantEquipped = matchItem and matchItem.enchantId and matchItem.enchantId == optimalItem.enchantId | |
| 293 | |
| 294 local lblEnchant = AceGUI:Create("AmrUiLabel") | |
| 295 lblEnchant:SetWordWrap(false) | |
| 296 lblEnchant:SetWidth(170) | |
| 297 lblEnchant:SetFont(Amr.CreateFont(isEnchantEquipped and "Regular" or "Bold", 14, isEnchantEquipped and Amr.Colors.TextGray or Amr.Colors.White)) | |
| 298 lblEnchant:SetPoint("TOPLEFT", lblItem.frame, "TOPRIGHT", 130, 0) | |
| 299 | |
| 300 local enchInfo = Amr.db.char.ExtraEnchantData[spec][optimalItem.enchantId] | |
| 301 if enchInfo then | |
| 302 lblEnchant:SetText(enchInfo.text) | |
| 303 | |
| 304 Amr.GetItemInfo(enchInfo.itemId, function(obj, name, link) | |
| 305 Amr:SetItemTooltip(obj, link) | |
| 306 end, lblEnchant) | |
| 307 --Amr:SetSpellTooltip(lblEnchant, enchInfo.spellId) | |
| 308 end | |
| 309 panelMods:AddChild(lblEnchant) | |
| 310 end | |
| 311 end | |
| 312 | |
| 313 prevElem = lbl | |
| 314 end | |
| 315 end | |
| 316 end | |
| 317 | |
| 318 local function onGearTabSelected(container, event, group) | |
| 319 container:ReleaseChildren() | |
| 320 _activeTab = group | |
| 321 renderGear(tonumber(group), container) | |
| 322 end | |
| 323 | |
| 324 local function onImportClick(widget) | |
| 325 Amr:ShowImportWindow() | |
| 326 end | |
| 327 | |
| 328 -- renders the main UI for the Gear tab | |
| 329 function Amr:RenderTabGear(container) | |
| 330 | |
| 331 local btnImport = AceGUI:Create("AmrUiButton") | |
| 332 btnImport:SetText(L.GearButtonImportText) | |
| 333 btnImport:SetBackgroundColor(Amr.Colors.Orange) | |
| 334 btnImport:SetFont(Amr.CreateFont("Bold", 16, Amr.Colors.White)) | |
| 335 btnImport:SetWidth(120) | |
| 336 btnImport:SetHeight(26) | |
| 337 btnImport:SetPoint("TOPLEFT", container.content, "TOPLEFT", 0, -81) | |
| 338 btnImport:SetCallback("OnClick", onImportClick) | |
| 339 container:AddChild(btnImport) | |
| 340 | |
| 341 local lbl = AceGUI:Create("AmrUiLabel") | |
| 342 lbl:SetText(L.GearImportNote) | |
| 343 lbl:SetWidth(100) | |
| 344 lbl:SetFont(Amr.CreateFont("Italic", 12, Amr.Colors.TextTan)) | |
| 345 lbl:SetJustifyH("MIDDLE") | |
| 346 lbl:SetPoint("TOP", btnImport.frame, "BOTTOM", 0, -5) | |
| 347 container:AddChild(lbl) | |
| 348 | |
| 349 local lbl2 = AceGUI:Create("AmrUiLabel") | |
| 350 lbl2:SetText(L.GearTipTitle) | |
| 351 lbl2:SetWidth(140) | |
| 352 lbl2:SetFont(Amr.CreateFont("Italic", 20, Amr.Colors.Text)) | |
| 353 lbl2:SetJustifyH("MIDDLE") | |
| 354 lbl2:SetPoint("TOP", lbl.frame, "BOTTOM", 0, -50) | |
| 355 container:AddChild(lbl2) | |
| 356 | |
| 357 lbl = AceGUI:Create("AmrUiLabel") | |
| 358 lbl:SetText(L.GearTipText) | |
| 359 lbl:SetWidth(140) | |
| 360 lbl:SetFont(Amr.CreateFont("Italic", 12, Amr.Colors.Text)) | |
| 361 lbl:SetJustifyH("MIDDLE") | |
| 362 lbl:SetPoint("TOP", lbl2.frame, "BOTTOM", 0, -5) | |
| 363 container:AddChild(lbl) | |
| 364 | |
| 365 lbl2 = AceGUI:Create("AmrUiLabel") | |
| 366 lbl2:SetText(L.GearTipCommands) | |
| 367 lbl2:SetWidth(130) | |
| 368 lbl2:SetFont(Amr.CreateFont("Italic", 12, Amr.Colors.Text)) | |
| 369 lbl2:SetPoint("TOP", lbl.frame, "BOTTOM", 10, -5) | |
| 370 container:AddChild(lbl2) | |
| 371 | |
| 372 --[[ | |
| 373 local btnClean = AceGUI:Create("AmrUiButton") | |
| 374 btnClean:SetText(L.GearButtonCleanText) | |
| 375 btnClean:SetBackgroundColor(Amr.Colors.Orange) | |
| 376 btnClean:SetFont(Amr.CreateFont("Bold", 16, Amr.Colors.White)) | |
| 377 btnClean:SetWidth(120) | |
| 378 btnClean:SetHeight(26) | |
| 379 btnClean:SetPoint("BOTTOMLEFT", container.content, "BOTTOMLEFT", 0, 5) | |
| 380 btnClean:SetCallback("OnClick", function(widget) Amr:CleanBags() end) | |
| 381 container:AddChild(btnClean) | |
| 382 ]] | |
| 383 | |
| 384 local t = AceGUI:Create("AmrUiTabGroup") | |
| 385 t:SetLayout("None") | |
| 386 t:SetTabs({ | |
| 387 {text=L.GearTabPrimary, value="1", style="bold"}, | |
| 388 {text=L.GearTabSecondary, value="2", style="bold"} | |
| 389 }) | |
| 390 t:SetCallback("OnGroupSelected", onGearTabSelected) | |
| 391 t:SetPoint("TOPLEFT", container.content, "TOPLEFT", 144, -30) | |
| 392 t:SetPoint("BOTTOMRIGHT", container.content, "BOTTOMRIGHT") | |
| 393 container:AddChild(t) | |
| 394 _gearTabs = t; | |
| 395 | |
| 396 if not _activeTab then | |
| 397 _activeTab = tostring(GetActiveSpecGroup()) | |
| 398 end | |
| 399 | |
| 400 t:SelectTab(_activeTab) | |
| 401 end | |
| 402 | |
| 403 -- do cleanup when the gear tab is released | |
| 404 function Amr:ReleaseTabGear() | |
| 405 _gearTabs = nil | |
| 406 end | |
| 407 | |
| 408 -- show and update the gear tab for the specified spec | |
| 409 function Amr:ShowGearTab(spec) | |
| 410 if not _gearTabs then return end | |
| 411 | |
| 412 _activeTab = tostring(spec) | |
| 413 _gearTabs:SelectTab(_activeTab) | |
| 414 end | |
| 415 | |
| 416 -- refresh display of the current gear tab | |
| 417 function Amr:RefreshGearTab() | |
| 418 if not _gearTabs then return end | |
| 419 _gearTabs:SelectTab(_activeTab) | |
| 420 end | |
| 421 | |
| 422 | |
| 423 ------------------------------------------------------------------------------------------------ | |
| 424 -- Gear Set Management | |
| 425 ------------------------------------------------------------------------------------------------ | |
| 426 local _waitingForSpec = 0 | |
| 427 local _waitingForItemLock = nil | |
| 428 local _pendingEquip = nil | |
| 429 | |
| 430 -- scan a bag for the best matching item | |
| 431 local function scanBagForItem(item, bagId, bestItem, bestDiff, bestLink) | |
| 432 local numSlots = GetContainerNumSlots(bagId) | |
| 433 for slotId = 1, numSlots do | |
| 434 local _, _, _, _, _, _, itemLink = GetContainerItemInfo(bagId, slotId) | |
| 435 -- we skip any stackable item, as far as we know, there is no equippable gear that can be stacked | |
| 436 if itemLink then | |
| 437 local bagItem = Amr.ParseItemLink(itemLink) | |
| 438 if bagItem ~= nil then | |
| 439 local diff = countItemDifferences(item, bagItem) | |
| 440 if diff < bestDiff then | |
| 441 bestItem = { bag = bagId, slot = slotId } | |
| 442 bestDiff = diff | |
| 443 bestLink = itemLink | |
| 444 end | |
| 445 end | |
| 446 end | |
| 447 end | |
| 448 return bestItem, bestDiff, bestLink | |
| 449 end | |
| 450 | |
| 451 -- find the first empty slot in the player's backpack+bags | |
| 452 local function findFirstEmptyBagSlot() | |
| 453 | |
| 454 local bagIds = {} | |
| 455 table.insert(bagIds, BACKPACK_CONTAINER) | |
| 456 for bagId = 1, NUM_BAG_SLOTS do | |
| 457 table.insert(bagIds, bagId) | |
| 458 end | |
| 459 | |
| 460 for i, bagId in ipairs(bagIds) do | |
| 461 local numSlots = GetContainerNumSlots(bagId) | |
| 462 for slotId = 1, numSlots do | |
| 463 local _, _, _, _, _, _, itemLink = GetContainerItemInfo(bagId, slotId) | |
| 464 if not itemLink then | |
| 465 return bagId, slotId | |
| 466 end | |
| 467 end | |
| 468 end | |
| 469 | |
| 470 return nil, nil | |
| 471 end | |
| 472 | |
| 473 local function finishEquipGearSet() | |
| 474 if not _pendingEquip then return end | |
| 475 | |
| 476 _pendingEquip.tries = _pendingEquip.tries + 1 | |
| 477 if _pendingEquip.tries > 16 then | |
| 478 _pendingEquip = nil | |
| 479 else | |
| 480 -- start over again, trying any items that could not be equipped in the previous pass (unique constraints) | |
| 481 Amr:EquipGearSet(_pendingEquip.spec) | |
| 482 end | |
| 483 end | |
| 484 | |
| 485 -- equip the next slot in a pending equip | |
| 486 local function tryEquipNextItem() | |
| 487 if not _pendingEquip then return end | |
| 488 | |
| 489 local item = _pendingEquip.itemsToEquip[_pendingEquip.nextSlot] | |
| 490 | |
| 491 local bestItem = nil | |
| 492 local bestLink = nil | |
| 493 local bestDiff = 1000 | |
| 494 | |
| 495 -- find the best matching item | |
| 496 | |
| 497 -- equipped items | |
| 498 for slotNum = 1, #Amr.SlotIds do | |
| 499 local slotId = Amr.SlotIds[slotNum] | |
| 500 local itemLink = GetInventoryItemLink("player", slotId) | |
| 501 if itemLink then | |
| 502 local invItem = Amr.ParseItemLink(itemLink) | |
| 503 if invItem ~= nil then | |
| 504 local diff = countItemDifferences(item, invItem) | |
| 505 if diff < bestDiff then | |
| 506 bestItem = { slot = slotId } | |
| 507 bestDiff = diff | |
| 508 bestLink = itemLink | |
| 509 end | |
| 510 end | |
| 511 end | |
| 512 end | |
| 513 | |
| 514 -- inventory | |
| 515 bestItem, bestDiff, bestLink = scanBagForItem(item, BACKPACK_CONTAINER, bestItem, bestDiff, bestLink) | |
| 516 for bagId = 1, NUM_BAG_SLOTS do | |
| 517 bestItem, bestDiff, bestLink = scanBagForItem(item, bagId, bestItem, bestDiff, bestLink) | |
| 518 end | |
| 519 | |
| 520 -- bank | |
| 521 bestItem, bestDiff = scanBagForItem(item, BANK_CONTAINER, bestItem, bestDiff, bestLink) | |
| 522 for bagId = NUM_BAG_SLOTS + 1, NUM_BAG_SLOTS + NUM_BANKBAGSLOTS do | |
| 523 bestItem, bestDiff = scanBagForItem(item, bagId, bestItem, bestDiff, bestLink) | |
| 524 end | |
| 525 | |
| 526 ClearCursor() | |
| 527 | |
| 528 if not bestItem then | |
| 529 -- stop if we can't find an item | |
| 530 Amr:Print(L.GearEquipErrorNotFound) | |
| 531 Amr:Print(L.GearEquipErrorNotFound2) | |
| 532 _pendingEquip = nil | |
| 533 return | |
| 534 | |
| 535 elseif bestItem and bestItem.bag and bestItem.bag >= NUM_BAG_SLOTS + 1 and bestItem.bag <= NUM_BAG_SLOTS + NUM_BANKBAGSLOTS then | |
| 536 -- find first empty bag slot | |
| 537 local invBag, invSlot = findFirstEmptyBagSlot() | |
| 538 if not invBag then | |
| 539 -- stop if bags are too full | |
| 540 Amr:Print(L.GearEquipErrorBagFull) | |
| 541 _pendingEquip = nil | |
| 542 return | |
| 543 end | |
| 544 | |
| 545 -- move from bank to bag | |
| 546 PickupContainerItem(bestItem.bag, bestItem.slot) | |
| 547 PickupContainerItem(invBag, invSlot) | |
| 548 | |
| 549 -- set flag so that when we clear cursor and release the item lock, we can respond to the event and continue | |
| 550 _waitingForItemLock = { | |
| 551 bagId = invBag, | |
| 552 slotId = invSlot | |
| 553 } | |
| 554 | |
| 555 ClearCursor() | |
| 556 | |
| 557 -- now we need to wait for game event to continue and try this item again after it is in our bag | |
| 558 return | |
| 559 else | |
| 560 if not Amr:IsSoulbound(bestItem.bag, bestItem.slot) then | |
| 561 -- if an item is not soulbound, then warn the user and quit | |
| 562 Amr:Print(L.GearEquipErrorSoulbound(bestLink)) | |
| 563 _pendingEquip = nil | |
| 564 return | |
| 565 else | |
| 566 local slotId = _pendingEquip.nextSlot | |
| 567 | |
| 568 -- an item in the player's bags or already equipped, equip it | |
| 569 _pendingEquip.bag = bestItem.bag | |
| 570 _pendingEquip.slot = bestItem.slot | |
| 571 _pendingEquip.destSlot = slotId | |
| 572 | |
| 573 if bestItem.bag then | |
| 574 PickupContainerItem(bestItem.bag, bestItem.slot) | |
| 575 else | |
| 576 PickupInventoryItem(bestItem.slot) | |
| 577 end | |
| 578 PickupInventoryItem(slotId) | |
| 579 ClearCursor() | |
| 580 end | |
| 581 end | |
| 582 | |
| 583 end | |
| 584 | |
| 585 local function onItemUnlocked(bagId, slotId) | |
| 586 | |
| 587 if _waitingForItemLock then | |
| 588 -- waiting on a move from bank to bags to complete, just continue as normal afterwards | |
| 589 if bagId == _waitingForItemLock.bagId and slotId == _waitingForItemLock.slotId then | |
| 590 _waitingForItemLock = nil | |
| 591 tryEquipNextItem() | |
| 592 end | |
| 593 | |
| 594 elseif _pendingEquip and _pendingEquip.destSlot then | |
| 595 -- waiting on an item swap to complete successfully so that we can go on to the next item | |
| 596 | |
| 597 -- inventory slot we're swapping to is still locked, can't continue yet | |
| 598 if IsInventoryItemLocked(_pendingEquip.destSlot) then return end | |
| 599 | |
| 600 if _pendingEquip.bag then | |
| 601 local _, _, locked = GetContainerItemInfo(_pendingEquip.bag, _pendingEquip.slot) | |
| 602 -- the bag slot we're swapping from is still locked, can't continue yet | |
| 603 if locked then return end | |
| 604 else | |
| 605 -- inventory slot we're swapping from is still locked, can't continue yet | |
| 606 if IsInventoryItemLocked(_pendingEquip.slot) then return end | |
| 607 end | |
| 608 | |
| 609 -- move on to the next item, this item is done | |
| 610 _pendingEquip.itemsToEquip[_pendingEquip.destSlot] = nil | |
| 611 _pendingEquip.destSlot = nil | |
| 612 _pendingEquip.bag = nil | |
| 613 _pendingEquip.slot = nil | |
| 614 | |
| 615 _pendingEquip.remaining = _pendingEquip.remaining - 1 | |
| 616 if _pendingEquip.remaining > 0 then | |
| 617 for slotId, item in pairs(_pendingEquip.itemsToEquip) do | |
| 618 _pendingEquip.nextSlot = slotId | |
| 619 break | |
| 620 end | |
| 621 tryEquipNextItem() | |
| 622 else | |
| 623 finishEquipGearSet() | |
| 624 end | |
| 625 | |
| 626 end | |
| 627 end | |
| 628 | |
| 629 local function startEquipGearSet(spec) | |
| 630 | |
| 631 local gear = Amr.db.char.GearSets[spec] | |
| 632 if not gear then | |
| 633 Amr:Print(L.GearEquipErrorEmpty) | |
| 634 return | |
| 635 end | |
| 636 | |
| 637 local player = Amr:ExportCharacter() | |
| 638 | |
| 639 local itemsToEquip = {} | |
| 640 local remaining = 0 | |
| 641 local usedItems = {} | |
| 642 local firstSlot = nil | |
| 643 | |
| 644 -- check for items that need to be equipped | |
| 645 for slotNum = 1, #Amr.SlotIds do | |
| 646 local slotId = Amr.SlotIds[slotNum] | |
| 647 | |
| 648 local old = player.Equipped[spec][slotId] | |
| 649 old = Amr.ParseItemLink(old) | |
| 650 | |
| 651 local new = gear[slotId] | |
| 652 | |
| 653 local diff = countItemDifferences(old, new) | |
| 654 if diff < 1000 then | |
| 655 -- same item, see if inventory has one that is closer (e.g. a duplicate item with correct enchants/gems) | |
| 656 local bestLink, bestItem, bestDiff = Amr:FindMatchingItem(new, player, usedItems) | |
| 657 if bestDiff and bestDiff < diff then | |
| 658 itemsToEquip[slotId] = new | |
| 659 remaining = remaining + 1 | |
| 660 end | |
| 661 else | |
| 662 itemsToEquip[slotId] = new | |
| 663 remaining = remaining + 1 | |
| 664 end | |
| 665 end | |
| 666 | |
| 667 if remaining > 0 then | |
| 668 _pendingEquip = { | |
| 669 tries = _pendingEquip and _pendingEquip.spec == spec and _pendingEquip.tries or 0, | |
| 670 spec = spec, | |
| 671 itemsToEquip = itemsToEquip, | |
| 672 remaining = remaining, | |
| 673 nextSlot = firstSlot | |
| 674 } | |
| 675 | |
| 676 -- starting item | |
| 677 for slotId, item in pairs(_pendingEquip.itemsToEquip) do | |
| 678 _pendingEquip.nextSlot = slotId | |
| 679 break | |
| 680 end | |
| 681 | |
| 682 tryEquipNextItem() | |
| 683 else | |
| 684 _pendingEquip = nil | |
| 685 end | |
| 686 end | |
| 687 | |
| 688 local function onActiveTalentGroupChanged() | |
| 689 local auto = Amr.db.profile.options.autoGear | |
| 690 local currentSpec = GetActiveSpecGroup() | |
| 691 | |
| 692 if currentSpec == _waitingForSpec or auto then | |
| 693 -- spec is what we want, now equip the gear | |
| 694 startEquipGearSet(currentSpec) | |
| 695 end | |
| 696 | |
| 697 _waitingForSpec = 0 | |
| 698 end | |
| 699 | |
| 700 -- activate the specified spec and then equip the saved gear set for either primary (1) or secondary (2) spec | |
| 701 function Amr:EquipGearSet(spec) | |
| 702 | |
| 703 -- if no argument, then toggle spec | |
| 704 if not spec then | |
| 705 spec = GetActiveSpecGroup() == 1 and 2 or 1 | |
| 706 end | |
| 707 | |
| 708 -- allow some flexibility in the arguments | |
| 709 if spec == "primary" or spec == "Primary" then spec = 1 end | |
| 710 if spec == "secondary" or spec == "Secondary" then spec = 2 end | |
| 711 if spec == "1" or spec == "2" then spec = tonumber(spec) end | |
| 712 | |
| 713 -- only spec 1 or 2 are valid | |
| 714 if spec ~= 1 and spec ~= 2 then return end | |
| 715 | |
| 716 if UnitAffectingCombat("player") then | |
| 717 Amr:Print(L.GearEquipErrorCombat) | |
| 718 return | |
| 719 end | |
| 720 | |
| 721 _waitingForSpec = spec | |
| 722 | |
| 723 local currentSpec = GetActiveSpecGroup() | |
| 724 if currentSpec ~= spec then | |
| 725 SetActiveSpecGroup(spec) | |
| 726 else | |
| 727 onActiveTalentGroupChanged() | |
| 728 end | |
| 729 end | |
| 730 | |
| 731 -- moves any gear in bags to the bank if not part of main or off spec gear set | |
| 732 function Amr:CleanBags() | |
| 733 -- TODO: implement | |
| 734 end | |
| 735 | |
| 736 function Amr:InitializeGear() | |
| 737 Amr:AddEventHandler("ACTIVE_TALENT_GROUP_CHANGED", onActiveTalentGroupChanged) | |
| 738 | |
| 739 Amr:AddEventHandler("UNIT_INVENTORY_CHANGED", function(unitID) | |
| 740 if unitID and unitID ~= "player" then return end | |
| 741 Amr:RefreshGearTab() | |
| 742 end) | |
| 743 | |
| 744 Amr:AddEventHandler("ITEM_UNLOCKED", onItemUnlocked) | |
| 745 end |
