Files
PathOfBuilding/src/Classes/ItemsTab.lua

3977 lines
158 KiB
Lua

-- Path of Building
--
-- Module: Items Tab
-- Items tab for the current build.
--
local pairs = pairs
local ipairs = ipairs
local next = next
local t_insert = table.insert
local t_remove = table.remove
local s_format = string.format
local m_max = math.max
local m_min = math.min
local m_ceil = math.ceil
local m_floor = math.floor
local m_modf = math.modf
local rarityDropList = {
{ label = colorCodes.NORMAL.."Normal", rarity = "NORMAL" },
{ label = colorCodes.MAGIC.."Magic", rarity = "MAGIC" },
{ label = colorCodes.RARE.."Rare", rarity = "RARE" },
{ label = colorCodes.UNIQUE.."Unique", rarity = "UNIQUE" },
{ label = colorCodes.RELIC.."Relic", rarity = "RELIC" }
}
local socketDropList = {
{ label = colorCodes.STRENGTH.."R", color = "R" },
{ label = colorCodes.DEXTERITY.."G", color = "G" },
{ label = colorCodes.INTELLIGENCE.."B", color = "B" },
{ label = colorCodes.SCION.."W", color = "W" }
}
local baseSlots = { "Weapon 1", "Weapon 2", "Helmet", "Body Armour", "Gloves", "Boots", "Amulet", "Ring 1", "Ring 2", "Ring 3", "Belt", "Flask 1", "Flask 2", "Flask 3", "Flask 4", "Flask 5" }
local influenceInfo = itemLib.influenceInfo.all
local catalystQualityFormat = {
"^x7F7F7FQuality (Attack Modifiers): "..colorCodes.MAGIC.."+%d%% (augmented)",
"^x7F7F7FQuality (Speed Modifiers): "..colorCodes.MAGIC.."+%d%% (augmented)",
"^x7F7F7FQuality (Life and Mana Modifiers): "..colorCodes.MAGIC.."+%d%% (augmented)",
"^x7F7F7FQuality (Caster Modifiers): "..colorCodes.MAGIC.."+%d%% (augmented)",
"^x7F7F7FQuality (Attribute Modifiers): "..colorCodes.MAGIC.."+%d%% (augmented)",
"^x7F7F7FQuality (Physical and Chaos Modifiers): "..colorCodes.MAGIC.."+%d%% (augmented)",
"^x7F7F7FQuality (Resistance Modifiers): "..colorCodes.MAGIC.."+%d%% (augmented)",
"^x7F7F7FQuality (Defense Modifiers): "..colorCodes.MAGIC.."+%d%% (augmented)",
"^x7F7F7FQuality (Elemental Modifiers): "..colorCodes.MAGIC.."+%d%% (augmented)",
"^x7F7F7FQuality (Critical Modifiers): "..colorCodes.MAGIC.."+%d%% (augmented)",
}
local flavourLookup = {}
for _, entry in pairs(data.flavourText) do
if entry.name and entry.id and entry.text then
flavourLookup[entry.name] = flavourLookup[entry.name] or {}
flavourLookup[entry.name][entry.id] = entry.text
end
end
local ItemsTabClass = newClass("ItemsTab", "UndoHandler", "ControlHost", "Control", function(self, build)
self.UndoHandler()
self.ControlHost()
self.Control()
self.build = build
self.socketViewer = new("PassiveTreeView")
self.items = { }
self.itemOrderList = { }
self.showStatDifferences = true
-- PoB Trader class initialization
self.tradeQuery = new("TradeQuery", self)
-- Set selector
self.controls.setSelect = new("DropDownControl", {"TOPLEFT",self,"TOPLEFT"}, {96, 8, 216, 20}, nil, function(index, value)
self:SetActiveItemSet(self.itemSetOrderList[index])
self:AddUndoState()
end)
self.controls.setSelect.enableDroppedWidth = true
self.controls.setSelect.enabled = function()
return #self.itemSetOrderList > 1
end
self.controls.setSelect.tooltipFunc = function(tooltip, mode, index, value)
tooltip:Clear()
if mode == "HOVER" then
self:AddItemSetTooltip(tooltip, self.itemSets[self.itemSetOrderList[index]])
end
end
self.controls.setLabel = new("LabelControl", {"RIGHT",self.controls.setSelect,"LEFT"}, {-2, 0, 0, 16}, "^7Item set:")
self.controls.setManage = new("ButtonControl", {"LEFT",self.controls.setSelect,"RIGHT"}, {4, 0, 90, 20}, "Manage...", function()
self:OpenItemSetManagePopup()
end)
-- Price Items
self.controls.priceDisplayItem = new("ButtonControl", {"TOPLEFT",self,"TOPLEFT"}, {96, 32, 310, 20}, "Trade for these items", function()
self.tradeQuery:PriceItem()
end)
self.controls.priceDisplayItem.tooltipFunc = function(tooltip)
tooltip:Clear()
tooltip:AddLine(16, "^7Contains searches from the official trading site to help find")
tooltip:AddLine(16, "^7similar or better items for this build")
end
-- Item slots
self.slots = { }
self.orderedSlots = { }
self.slotOrder = { }
self.slotAnchor = new("Control", {"TOPLEFT",self,"TOPLEFT"}, {96, 76, 310, 0})
local prevSlot = self.slotAnchor
local function addSlot(slot)
prevSlot = slot
self.slots[slot.slotName] = slot
t_insert(self.orderedSlots, slot)
self.slotOrder[slot.slotName] = #self.orderedSlots
t_insert(self.controls, slot)
end
for index, slotName in ipairs(baseSlots) do
local slot = new("ItemSlotControl", {"TOPLEFT",prevSlot,"BOTTOMLEFT"}, 0, 2, self, slotName)
addSlot(slot)
if slotName:match("Weapon") then
-- Add alternate weapon slot
slot.weaponSet = 1
slot.shown = function()
return not self.activeItemSet.useSecondWeaponSet
end
local swapSlot = new("ItemSlotControl", {"TOPLEFT",prevSlot,"BOTTOMLEFT"}, 0, 2, self, slotName.." Swap", slotName)
addSlot(swapSlot)
swapSlot.weaponSet = 2
swapSlot.shown = function()
return self.activeItemSet.useSecondWeaponSet
end
for i = 1, 6 do
local abyssal = new("ItemSlotControl", {"TOPLEFT",prevSlot,"BOTTOMLEFT"}, 0, 2, self, slotName.." Swap Abyssal Socket "..i, "Abyssal #"..i)
addSlot(abyssal)
abyssal.parentSlot = swapSlot
abyssal.weaponSet = 2
abyssal.shown = function()
return not abyssal.inactive and self.activeItemSet.useSecondWeaponSet
end
swapSlot.abyssalSocketList[i] = abyssal
end
elseif slotName == "Ring 3" then
slot.shown = function()
return self.build.calcsTab.mainEnv.modDB:Flag(nil, "AdditionalRingSlot")
end
end
if slotName == "Weapon 1" or slotName == "Weapon 2" or slotName == "Helmet" or slotName == "Gloves" or slotName == "Body Armour" or slotName == "Boots" or slotName == "Belt" then
-- Add Abyssal Socket slots
for i = 1, 6 do
local abyssal = new("ItemSlotControl", {"TOPLEFT",prevSlot,"BOTTOMLEFT"}, 0, 2, self, slotName.." Abyssal Socket "..i, "Abyssal #"..i)
addSlot(abyssal)
abyssal.parentSlot = slot
if slotName:match("Weapon") then
abyssal.weaponSet = 1
abyssal.shown = function()
return not abyssal.inactive and not self.activeItemSet.useSecondWeaponSet
end
end
slot.abyssalSocketList[i] = abyssal
end
end
end
-- Passive tree dropdown controls
self.controls.specSelect = new("DropDownControl", {"TOPLEFT",prevSlot,"BOTTOMLEFT"}, {0, 8, 216, 20}, nil, function(index, value)
if self.build.treeTab.specList[index] then
self.build.modFlag = true
self.build.treeTab:SetActiveSpec(index)
end
end)
self.controls.specSelect.enabled = function()
return #self.controls.specSelect.list > 1
end
prevSlot = self.controls.specSelect
self.controls.specButton = new("ButtonControl", {"LEFT",prevSlot,"RIGHT"}, {4, 0, 90, 20}, "Manage...", function()
self.build.treeTab:OpenSpecManagePopup()
end)
self.controls.specLabel = new("LabelControl", {"RIGHT",prevSlot,"LEFT"}, {-2, 0, 0, 16}, "^7Passive tree:")
self.sockets = { }
local socketOrder = { }
for _, node in pairs(build.latestTree.nodes) do
if node.type == "Socket" then
t_insert(socketOrder, node)
end
end
table.sort(socketOrder, function(a, b)
return a.id < b.id
end)
for _, node in ipairs(socketOrder) do
local socketControl = new("ItemSlotControl", {"TOPLEFT",prevSlot,"BOTTOMLEFT"}, 0, 2, self, "Jewel "..node.id, "Socket", node.id)
self.sockets[node.id] = socketControl
addSlot(socketControl)
end
self.controls.slotHeader = new("LabelControl", {"BOTTOMLEFT",self.slotAnchor,"TOPLEFT"}, {0, -4, 0, 16}, "^7Equipped items:")
self.controls.weaponSwap1 = new("ButtonControl", {"BOTTOMRIGHT",self.slotAnchor,"TOPRIGHT"}, {-20, -2, 18, 18}, "I", function()
if self.activeItemSet.useSecondWeaponSet then
self.activeItemSet.useSecondWeaponSet = false
self:AddUndoState()
self.build.buildFlag = true
local mainSocketGroup = self.build.skillsTab.socketGroupList[self.build.mainSocketGroup]
if mainSocketGroup and mainSocketGroup.slot and self.slots[mainSocketGroup.slot].weaponSet == 2 then
for index, socketGroup in ipairs(self.build.skillsTab.socketGroupList) do
if socketGroup.slot and self.slots[socketGroup.slot].weaponSet == 1 then
self.build.mainSocketGroup = index
break
end
end
end
end
end)
self.controls.weaponSwap1.overSizeText = 3
self.controls.weaponSwap1.locked = function()
return not self.activeItemSet.useSecondWeaponSet
end
self.controls.weaponSwap2 = new("ButtonControl", {"BOTTOMRIGHT",self.slotAnchor,"TOPRIGHT"}, {0, -2, 18, 18}, "II", function()
if not self.activeItemSet.useSecondWeaponSet then
self.activeItemSet.useSecondWeaponSet = true
self:AddUndoState()
self.build.buildFlag = true
local mainSocketGroup = self.build.skillsTab.socketGroupList[self.build.mainSocketGroup]
if mainSocketGroup and mainSocketGroup.slot and self.slots[mainSocketGroup.slot].weaponSet == 1 then
for index, socketGroup in ipairs(self.build.skillsTab.socketGroupList) do
if socketGroup.slot and self.slots[socketGroup.slot].weaponSet == 2 then
self.build.mainSocketGroup = index
break
end
end
end
end
end)
self.controls.weaponSwap2.overSizeText = 3
self.controls.weaponSwap2.locked = function()
return self.activeItemSet.useSecondWeaponSet
end
self.controls.weaponSwapLabel = new("LabelControl", {"RIGHT",self.controls.weaponSwap1,"LEFT"}, {-4, 0, 0, 14}, "^7Weapon Set:")
-- All items list
if main.portraitMode then
self.controls.itemList = new("ItemListControl", {"TOPRIGHT",self.lastSlot,"BOTTOMRIGHT"}, {0, 0, 360, 308}, self, true)
else
self.controls.itemList = new("ItemListControl", {"TOPLEFT",self.controls.setManage,"TOPRIGHT"}, {20, 20, 360, 308}, self, true)
end
-- Database selector
self.controls.selectDBLabel = new("LabelControl", {"TOPLEFT",self.controls.itemList,"BOTTOMLEFT"}, {0, 14, 0, 16}, "^7Import from:")
self.controls.selectDBLabel.shown = function()
return self.height < 980
end
self.controls.selectDB = new("DropDownControl", {"LEFT",self.controls.selectDBLabel,"RIGHT"}, {4, 0, 150, 18}, { "Uniques", "Rare Templates" })
-- Unique database
self.controls.uniqueDB = new("ItemDBControl", {"TOPLEFT",self.controls.itemList,"BOTTOMLEFT"}, {0, 76, 360, function(c) return m_min(244, self.maxY - select(2, c:GetPos())) end}, self, main.uniqueDB, "UNIQUE")
self.controls.uniqueDB.y = function()
return self.controls.selectDBLabel:IsShown() and 118 or 96
end
self.controls.uniqueDB.shown = function()
return not self.controls.selectDBLabel:IsShown() or self.controls.selectDB.selIndex == 1
end
-- Rare template database
self.controls.rareDB = new("ItemDBControl", {"TOPLEFT",self.controls.itemList,"BOTTOMLEFT"}, {0, 76, 360, function(c) return m_min(260, self.maxY - select(2, c:GetPos())) end}, self, main.rareDB, "RARE")
self.controls.rareDB.y = function()
return self.controls.selectDBLabel:IsShown() and 78 or 396
end
self.controls.rareDB.shown = function()
return not self.controls.selectDBLabel:IsShown() or self.controls.selectDB.selIndex == 2
end
-- Create/import item
self.controls.craftDisplayItem = new("ButtonControl", {"TOPLEFT",main.portraitMode and self.controls.setManage or self.controls.itemList,"TOPRIGHT"}, {20, main.portraitMode and 0 or -20, 120, 20}, "Craft item...", function()
self:CraftItem()
end)
self.controls.craftDisplayItem.shown = function()
return self.displayItem == nil
end
self.controls.newDisplayItem = new("ButtonControl", {"TOPLEFT",self.controls.craftDisplayItem,"TOPRIGHT"}, {8, 0, 120, 20}, "Create custom...", function()
self:EditDisplayItemText()
end)
self.controls.displayItemTip = new("LabelControl", {"TOPLEFT",self.controls.craftDisplayItem,"BOTTOMLEFT"}, {0, 8, 100, 16},
[[^7Double-click an item from one of the lists,
or copy and paste an item from in game
(hover over the item and Ctrl+C) to view or edit
the item and add it to your build. You can
also clone an item within Path of Building by
copying and pasting it with Ctrl+C and Ctrl+V.
You can Control + Click an item to equip it, or
drag it onto the slot. This will also add it to
your build if it's from the unique/template list.
If there's 2 slots an item can go in,
holding Shift will put it in the second.]])
self.controls.sharedItemList = new("SharedItemListControl", {"TOPLEFT",self.controls.craftDisplayItem, "BOTTOMLEFT"}, {0, 232, 340, 308}, self, true)
-- Display item
self.displayItemTooltip = new("Tooltip")
self.displayItemTooltip.maxWidth = 458
self.anchorDisplayItem = new("Control", {"TOPLEFT",main.portraitMode and self.controls.setManage or self.controls.itemList,"TOPRIGHT"}, {20, main.portraitMode and 0 or -20, 0, 0})
self.anchorDisplayItem.shown = function()
return self.displayItem ~= nil
end
self.controls.addDisplayItem = new("ButtonControl", {"TOPLEFT",self.anchorDisplayItem,"TOPLEFT"}, {0, 0, 100, 20}, "", function()
self:AddDisplayItem()
end)
self.controls.addDisplayItem.label = function()
return self.items[self.displayItem.id] and "Save" or "Add to build"
end
self.controls.editDisplayItem = new("ButtonControl", {"LEFT",self.controls.addDisplayItem,"RIGHT"}, {8, 0, 60, 20}, "Edit...", function()
self:EditDisplayItemText()
end)
self.controls.removeDisplayItem = new("ButtonControl", {"LEFT",self.controls.editDisplayItem,"RIGHT"}, {8, 0, 60, 20}, "Cancel", function()
self:SetDisplayItem()
end)
-- Section: Variant(s)
self.controls.displayItemSectionVariant = new("Control", {"TOPLEFT",self.controls.addDisplayItem,"BOTTOMLEFT"}, {0, 8, 0, function()
if not self.controls.displayItemVariant:IsShown() then
return 0
end
return (28 +
(self.displayItem.hasAltVariant and 24 or 0) +
(self.displayItem.hasAltVariant2 and 24 or 0) +
(self.displayItem.hasAltVariant3 and 24 or 0) +
(self.displayItem.hasAltVariant4 and 24 or 0) +
(self.displayItem.hasAltVariant5 and 24 or 0))
end})
self.controls.displayItemVariant = new("DropDownControl", {"TOPLEFT", self.controls.displayItemSectionVariant,"TOPLEFT"}, {0, 0, 300, 20}, nil, function(index, value)
self.displayItem.variant = index
self.displayItem:BuildAndParseRaw()
self:UpdateDisplayItemTooltip()
self:UpdateDisplayItemRangeLines()
end)
self.controls.displayItemVariant.maxDroppedWidth = 1000
self.controls.displayItemVariant.shown = function()
return self.displayItem.variantList and #self.displayItem.variantList > 1
end
self.controls.displayItemAltVariant = new("DropDownControl", {"TOPLEFT",self.controls.displayItemVariant,"BOTTOMLEFT"}, {0, 4, 300, 20}, nil, function(index, value)
self.displayItem.variantAlt = index
self.displayItem:BuildAndParseRaw()
self:UpdateDisplayItemTooltip()
self:UpdateDisplayItemRangeLines()
end)
self.controls.displayItemAltVariant.maxDroppedWidth = 1000
self.controls.displayItemAltVariant.shown = function()
return self.displayItem.hasAltVariant
end
self.controls.displayItemAltVariant2 = new("DropDownControl", {"TOPLEFT",self.controls.displayItemAltVariant,"BOTTOMLEFT"}, {0, 4, 300, 20}, nil, function(index, value)
self.displayItem.variantAlt2 = index
self.displayItem:BuildAndParseRaw()
self:UpdateDisplayItemTooltip()
self:UpdateDisplayItemRangeLines()
end)
self.controls.displayItemAltVariant2.maxDroppedWidth = 1000
self.controls.displayItemAltVariant2.shown = function()
return self.displayItem.hasAltVariant2
end
self.controls.displayItemAltVariant3 = new("DropDownControl", {"TOPLEFT",self.controls.displayItemAltVariant2,"BOTTOMLEFT"}, {0, 4, 300, 20}, nil, function(index, value)
self.displayItem.variantAlt3 = index
self.displayItem:BuildAndParseRaw()
self:UpdateDisplayItemTooltip()
self:UpdateDisplayItemRangeLines()
end)
self.controls.displayItemAltVariant3.maxDroppedWidth = 1000
self.controls.displayItemAltVariant3.shown = function()
return self.displayItem.hasAltVariant3
end
self.controls.displayItemAltVariant4 = new("DropDownControl", {"TOPLEFT",self.controls.displayItemAltVariant3,"BOTTOMLEFT"}, {0, 4, 300, 20}, nil, function(index, value)
self.displayItem.variantAlt4 = index
self.displayItem:BuildAndParseRaw()
self:UpdateDisplayItemTooltip()
self:UpdateDisplayItemRangeLines()
end)
self.controls.displayItemAltVariant4.maxDroppedWidth = 1000
self.controls.displayItemAltVariant4.shown = function()
return self.displayItem.hasAltVariant4
end
self.controls.displayItemAltVariant5 = new("DropDownControl", {"TOPLEFT",self.controls.displayItemAltVariant4,"BOTTOMLEFT"}, {0, 4, 300, 20}, nil, function(index, value)
self.displayItem.variantAlt5 = index
self.displayItem:BuildAndParseRaw()
self:UpdateDisplayItemTooltip()
self:UpdateDisplayItemRangeLines()
end)
self.controls.displayItemAltVariant5.maxDroppedWidth = 1000
self.controls.displayItemAltVariant5.shown = function()
return self.displayItem.hasAltVariant5
end
-- Section: Sockets and Links
self.controls.displayItemSectionSockets = new("Control", {"TOPLEFT",self.controls.displayItemSectionVariant,"BOTTOMLEFT"}, {0, 0, 0, function()
return self.displayItem and self.displayItem.selectableSocketCount > 0 and 28 or 0
end})
for i = 1, 6 do
local drop = new("DropDownControl", {"TOPLEFT",self.controls.displayItemSectionSockets,"TOPLEFT"}, {(i-1) * 64, 0, 36, 20}, socketDropList, function(index, value)
self.displayItem.sockets[i].color = value.color
self.displayItem:BuildAndParseRaw()
self:UpdateDisplayItemTooltip()
end)
drop.shown = function()
return self.displayItem.selectableSocketCount >= i and self.displayItem.sockets[i] and self.displayItem.sockets[i].color ~= "A"
end
self.controls["displayItemSocket"..i] = drop
if i < 6 then
local link = new("CheckBoxControl", {"LEFT",drop,"RIGHT"}, {4, 0, 20}, nil, function(state)
if state and self.displayItem.sockets[i].group ~= self.displayItem.sockets[i+1].group then
for s = i + 1, #self.displayItem.sockets do
self.displayItem.sockets[s].group = self.displayItem.sockets[s].group - 1
end
elseif not state and self.displayItem.sockets[i].group == self.displayItem.sockets[i+1].group then
for s = i + 1, #self.displayItem.sockets do
self.displayItem.sockets[s].group = self.displayItem.sockets[s].group + 1
end
end
self.displayItem:BuildAndParseRaw()
self:UpdateDisplayItemTooltip()
end)
link.shown = function()
return self.displayItem.selectableSocketCount > i and self.displayItem.sockets[i+1] and self.displayItem.sockets[i+1].color ~= "A"
end
self.controls["displayItemLink"..i] = link
end
end
self.controls.displayItemAddSocket = new("ButtonControl", {"TOPLEFT",self.controls.displayItemSectionSockets,"TOPLEFT"}, {function() return (#self.displayItem.sockets - self.displayItem.abyssalSocketCount) * 64 - 12 end, 0, 20, 20}, "+", function()
local insertIndex = #self.displayItem.sockets - self.displayItem.abyssalSocketCount + 1
t_insert(self.displayItem.sockets, insertIndex, {
color = self.displayItem.defaultSocketColor,
group = self.displayItem.sockets[insertIndex - 1].group + 1
})
for s = insertIndex + 1, #self.displayItem.sockets do
self.displayItem.sockets[s].group = self.displayItem.sockets[s].group + 1
end
self.displayItem:BuildAndParseRaw()
self:UpdateSocketControls()
self:UpdateDisplayItemTooltip()
end)
self.controls.displayItemAddSocket.shown = function()
return #self.displayItem.sockets < self.displayItem.selectableSocketCount + self.displayItem.abyssalSocketCount
end
-- Section: Enchant / Anoint / Corrupt
self.controls.displayItemSectionEnchant = new("Control", {"TOPLEFT",self.controls.displayItemSectionSockets,"BOTTOMLEFT"}, {0, 0, 0, function()
return (self.controls.displayItemEnchant:IsShown() or self.controls.displayItemEnchant2:IsShown() or self.controls.displayItemAnoint:IsShown() or self.controls.displayItemAnoint2:IsShown() or self.controls.displayItemCorrupt:IsShown() ) and 28 or 0
end})
self.controls.displayItemEnchant = new("ButtonControl", {"TOPLEFT",self.controls.displayItemSectionEnchant,"TOPLEFT"}, {0, 0, 160, 20}, "Apply Enchantment...", function()
self:EnchantDisplayItem(1)
end)
self.controls.displayItemEnchant.shown = function()
return self.displayItem and self.displayItem.enchantments
end
self.controls.displayItemEnchant2 = new("ButtonControl", {"TOPLEFT",self.controls.displayItemEnchant,"TOPRIGHT",true}, {8, 0, 160, 20}, "Apply Enchantment 2...", function()
self:EnchantDisplayItem(2)
end)
self.controls.displayItemEnchant2.shown = function()
return self.displayItem and self.displayItem.enchantments and self.displayItem.canHaveTwoEnchants and #self.displayItem.enchantModLines > 0
end
self.controls.displayItemAnoint = new("ButtonControl", {"TOPLEFT",self.controls.displayItemEnchant2,"TOPRIGHT",true}, {8, 0, 100, 20}, "Anoint...", function()
self:AnointDisplayItem(1)
end)
self.controls.displayItemAnoint.shown = function()
return self.displayItem and (self.displayItem.base.type == "Amulet" or self.displayItem.canBeAnointed)
end
self.controls.displayItemAnoint2 = new("ButtonControl", {"TOPLEFT",self.controls.displayItemAnoint,"TOPRIGHT",true}, {8, 0, 100, 20}, "Anoint 2...", function()
self:AnointDisplayItem(2)
end)
self.controls.displayItemAnoint2.shown = function()
return self.displayItem and
(self.displayItem.base.type == "Amulet" or self.displayItem.canBeAnointed) and
self.displayItem.canHaveTwoEnchants and
#self.displayItem.enchantModLines > 0
end
self.controls.displayItemAnoint3 = new("ButtonControl", {"TOPLEFT",self.controls.displayItemAnoint2,"TOPRIGHT",true}, {8, 0, 100, 20}, "Anoint 3...", function()
self:AnointDisplayItem(3)
end)
self.controls.displayItemAnoint3.shown = function()
return self.displayItem and
(self.displayItem.base.type == "Amulet" or self.displayItem.canBeAnointed) and
self.displayItem.canHaveThreeEnchants and
#self.displayItem.enchantModLines > 1
end
self.controls.displayItemAnoint4 = new("ButtonControl", {"TOPLEFT",self.controls.displayItemAnoint3,"TOPRIGHT",true}, {8, 0, 100, 20}, "Anoint 4...", function()
self:AnointDisplayItem(4)
end)
self.controls.displayItemAnoint4.shown = function()
return self.displayItem and
(self.displayItem.base.type == "Amulet" or self.displayItem.canBeAnointed) and
self.displayItem.canHaveFourEnchants and
#self.displayItem.enchantModLines > 2
end
self.controls.displayItemCorrupt = new("ButtonControl", {"TOPLEFT",self.controls.displayItemAnoint4,"TOPRIGHT",true}, {8, 10, 100, 20}, "Corrupt...", function()
self:CorruptDisplayItem("Corrupted")
end)
self.controls.displayItemCorrupt.shown = function()
return self.displayItem and self.displayItem.corruptible
end
--[[
self.controls.displayItemScourge = new("ButtonControl", {"TOPLEFT",self.controls.displayItemCorrupt,"TOPRIGHT",true}, {8, 0, 100, 20}, "Scourge...", function()
self:CorruptDisplayItem("Scourge")
end)
self.controls.displayItemScourge.shown = function()
return self.displayItem and self.displayItem.corruptible
end
--]]
self.controls.displayItemAddImplicit = new("ButtonControl", {"TOPLEFT",self.controls.displayItemCorrupt,"TOPRIGHT",true}, {8, 0, 120, 20}, "Add Implicit...", function()
self:AddImplicitToDisplayItem()
end)
self.controls.displayItemAddImplicit.shown = function()
return self.displayItem and
self.displayItem.type ~= "Tincture" and (self.displayItem.corruptible or ((self.displayItem.type ~= "Flask" and self.displayItem.type ~= "Jewel") and
(self.displayItem.rarity == "NORMAL" or self.displayItem.rarity == "MAGIC" or self.displayItem.rarity == "RARE"))) and
not self.displayItem.implicitsCannotBeChanged
end
-- Section: Influence dropdowns
local influenceDisplayList = { "Influence" }
for i, curInfluenceInfo in ipairs(influenceInfo) do
influenceDisplayList[i + 1] = curInfluenceInfo.display
end
local function setDisplayItemInfluence(influenceIndexList)
self.displayItem:ResetInfluence()
if self.displayItem.HasElderShaperAndAllConquerorInfluences then
for i, curInfluenceInfo in ipairs(itemLib.influenceInfo.default) do
self.displayItem[influenceInfo[i].key] = true
end
else
for _, index in ipairs(influenceIndexList) do
if index > 0 then
self.displayItem[influenceInfo[index].key] = true
end
end
end
if self.displayItem.crafted then
for i = 1, self.displayItem.affixLimit do
-- Force affix selectors to update
local drop = self.controls["displayItemAffix"..i]
drop.selFunc(drop.selIndex, drop.list[drop.selIndex])
end
end
self.displayItem:BuildAndParseRaw()
self:UpdateDisplayItemTooltip()
end
self.controls.displayItemSectionInfluence = new("Control", {"TOPLEFT",self.controls.displayItemSectionEnchant,"BOTTOMLEFT"}, {0, 0, 0, function()
return self.displayItem and self.displayItem.canBeInfluenced and 28 or 0
end})
self.controls.displayItemInfluence = new("DropDownControl", {"TOPLEFT",self.controls.displayItemSectionInfluence,"TOPRIGHT"}, {0, 0, 100, 20}, influenceDisplayList, function(index, value)
local otherIndex = self.controls.displayItemInfluence2.selIndex
setDisplayItemInfluence({ index - 1, otherIndex - 1 })
end)
self.controls.displayItemInfluence.shown = function()
return self.displayItem and self.displayItem.canBeInfluenced
end
self.controls.displayItemInfluence2 = new("DropDownControl", {"TOPLEFT",self.controls.displayItemInfluence,"TOPRIGHT",true}, {8, 0, 100, 20}, influenceDisplayList, function(index, value)
local otherIndex = self.controls.displayItemInfluence.selIndex
setDisplayItemInfluence({ index - 1, otherIndex - 1 })
end)
self.controls.displayItemInfluence2.shown = function()
return self.displayItem and self.displayItem.canBeInfluenced
end
-- Section: Item Quality
self.controls.displayItemSectionQuality = new("Control", {"TOPLEFT",self.controls.displayItemSectionInfluence,"BOTTOMLEFT"}, {0, 0, 0, function()
return (self.controls.displayItemQuality:IsShown() and self.controls.displayItemQualityEdit:IsShown()) and 28 or 0
end})
self.controls.displayItemQuality = new("LabelControl", {"TOPLEFT",self.controls.displayItemSectionQuality,"TOPRIGHT"}, {-4, 0, 0, 16}, "^7Quality:")
self.controls.displayItemQuality.shown = function()
return self.displayItem and self.displayItem.quality and (self.displayItem.base.type ~= "Amulet" or self.displayItem.base.type ~= "Belt" or self.displayItem.base.type ~= "Jewel" or self.displayItem.base.type ~= "Quiver" or self.displayItem.base.type ~= "Ring")
end
self.controls.displayItemQualityEdit = new("EditControl", {"LEFT",self.controls.displayItemQuality,"RIGHT"}, {2, 0, 60, 20}, nil, nil, "%D", 2, function(buf)
self.displayItem.quality = tonumber(buf)
self.displayItem:BuildAndParseRaw()
self:UpdateDisplayItemTooltip()
end)
self.controls.displayItemQualityEdit.shown = function()
return self.displayItem and self.displayItem.quality and (self.displayItem.base.type ~= "Amulet" or self.displayItem.base.type ~= "Belt" or self.displayItem.base.type ~= "Jewel" or self.displayItem.base.type ~= "Quiver" or self.displayItem.base.type ~= "Ring")
end
-- Section: Catalysts
self.controls.displayItemSectionCatalyst = new("Control", {"TOPLEFT",self.controls.displayItemSectionQuality,"BOTTOMLEFT"}, {0, 0, 0, function()
return (self.controls.displayItemCatalyst:IsShown() or self.controls.displayItemCatalystQualityEdit:IsShown()) and 28 or 0
end})
self.controls.displayItemCatalyst = new("DropDownControl", {"TOPLEFT",self.controls.displayItemSectionCatalyst,"TOPRIGHT"}, {0, 0, 250, 20},
{"Catalyst","Abrasive (Attack)","Accelerating (Speed)","Fertile (Life & Mana)","Imbued (Caster)","Intrinsic (Attribute)","Noxious (Physical & Chaos Damage)",
"Prismatic (Resistance)","Tempering (Defense)","Turbulent (Elemental)","Unstable (Critical)"},
function(index, value)
self.displayItem.catalyst = index - 1
if not self.displayItem.catalystQuality then
self.displayItem.catalystQuality = 20
self.controls.displayItemCatalystQualityEdit:SetText(self.displayItem.catalystQuality)
end
if self.displayItem.crafted then
for i = 1, self.displayItem.affixLimit do
-- Force affix selectors to update
local drop = self.controls["displayItemAffix"..i]
drop.selFunc(drop.selIndex, drop.list[drop.selIndex])
end
end
self.displayItem:BuildAndParseRaw()
self:UpdateDisplayItemTooltip()
end)
self.controls.displayItemCatalyst.shown = function()
return self.displayItem and (self.displayItem.crafted or self.displayItem.hasModTags) and (self.displayItem.base.type == "Amulet" or self.displayItem.base.type == "Ring" or self.displayItem.base.type == "Belt")
end
self.controls.displayItemCatalystQualityEdit = new("EditControl", {"LEFT",self.controls.displayItemCatalyst,"RIGHT"}, {2, 0, 60, 20}, nil, nil, "%D", 2, function(buf)
self.displayItem.catalystQuality = tonumber(buf)
if self.displayItem.crafted then
for i = 1, self.displayItem.affixLimit do
-- Force affix selectors to update
local drop = self.controls["displayItemAffix"..i]
drop.selFunc(drop.selIndex, drop.list[drop.selIndex])
end
end
self.displayItem:BuildAndParseRaw()
self:UpdateDisplayItemTooltip()
end)
self.controls.displayItemCatalystQualityEdit.shown = function()
return self.displayItem and (self.displayItem.crafted or self.displayItem.hasModTags) and self.displayItem.catalyst and self.displayItem.catalyst > 0
end
-- Section: Cluster Jewel
self.controls.displayItemSectionClusterJewel = new("Control", {"TOPLEFT",self.controls.displayItemSectionCatalyst,"BOTTOMLEFT"}, {0, 0, 0, function()
return self.controls.displayItemClusterJewelSkill:IsShown() and 52 or 0
end})
self.controls.displayItemClusterJewelSkill = new("DropDownControl", {"TOPLEFT",self.controls.displayItemSectionClusterJewel,"TOPLEFT"}, {0, 0, 300, 20}, { }, function(index, value)
self.displayItem.clusterJewelSkill = value.skillId
self:CraftClusterJewel()
end) {
shown = function()
return self.displayItem and self.displayItem.crafted and self.displayItem.clusterJewel
end
}
self.controls.displayItemClusterJewelNodeCountLabel = new("LabelControl", {"TOPLEFT",self.controls.displayItemClusterJewelSkill,"BOTTOMLEFT"}, {0, 7, 0, 14}, "^7Added Passives:")
self.controls.displayItemClusterJewelNodeCount = new("SliderControl", {"LEFT",self.controls.displayItemClusterJewelNodeCountLabel,"RIGHT"}, {2, 0, 150, 20}, function(val)
local divVal = self.controls.displayItemClusterJewelNodeCount:GetDivVal()
local clusterJewel = self.displayItem.clusterJewel
self.displayItem.clusterJewelNodeCount = round(val * (clusterJewel.maxNodes - clusterJewel.minNodes) + clusterJewel.minNodes)
self:CraftClusterJewel()
end)
-- Section: Affix Selection
self.controls.displayItemSectionAffix = new("Control", {"TOPLEFT",self.controls.displayItemSectionClusterJewel,"BOTTOMLEFT"}, {0, 0, 0, function()
if not self.displayItem or not self.displayItem.crafted then
return 0
end
local h = 6
for i = 1, 6 do
if self.controls["displayItemAffix"..i]:IsShown() then
h = h + 24
if self.controls["displayItemAffixRange"..i]:IsShown() then
h = h + 18
end
end
end
return h
end})
for i = 1, 6 do
local prev = self.controls["displayItemAffix"..(i-1)] or self.controls.displayItemSectionAffix
local drop, slider
local function verifyRange(range, index, drop) -- flips range if it will form discontinuous values
local priorMod = index - 1 > 0 and self.displayItem.affixes[drop.list[drop.selIndex].modList[index - 1]] or nil
local nextMod = index + 1 < #drop.list[drop.selIndex].modList and self.displayItem.affixes[drop.list[drop.selIndex].modList[index + 1]] or nil
local function flipRange(modA, modB) -- assumes all pairs are ordered the same
local function getMinMax(mod) -- gets first valid range from a mod
for _, line in ipairs(mod) do
local min, max = line:match("%((%d[%d%.]*)%-(%d[%d%.]*)%)")
if min and max then return tonumber(min), tonumber(max) end
end
end
local minA, maxA = getMinMax(modA)
local minB, maxB = getMinMax(modB)
if not minA or not minB or not maxA or not maxB then
return false
end
local allInts = minA == m_floor(minA) and maxA == m_floor(maxA) and minB == m_floor(minB) and maxB == m_floor(maxB) -- if the mod goes in steps that aren't 1, then the code below this doesn't work
if (minA and minB and maxA and maxB and allInts) then
if (minA < minB) then -- ascending
return minA + 1 == maxB
else -- descending
return minA - 1 == maxB
end
end
return false
end
if priorMod then
if flipRange(priorMod, self.displayItem.affixes[drop.list[drop.selIndex].modList[index]]) then
range = 1 - range
end
elseif nextMod then
if flipRange(self.displayItem.affixes[drop.list[drop.selIndex].modList[index]], nextMod) then
range = 1 - range
end
end
return range
end
drop = new("DropDownControl", {"TOPLEFT",prev,"TOPLEFT"}, {i==1 and 40 or 0, 0, 418, 20}, nil, function(index, value)
local affix = { modId = "None" }
if value.modId then
affix.modId = value.modId
affix.range = slider.val
elseif value.modList then
slider.divCount = #value.modList
local index, range = slider:GetDivVal()
affix.modId = value.modList[index]
affix.range = verifyRange(range, index, drop)
end
self.displayItem[drop.outputTable][drop.outputIndex] = affix
self.displayItem:Craft()
self:UpdateDisplayItemTooltip()
self:UpdateAffixControls()
end)
drop.y = function()
return i == 1 and 0 or 24 + (prev.slider:IsShown() and 18 or 0)
end
drop.tooltipFunc = function(tooltip, mode, index, value)
local modList = value.modList
if not modList or main.popups[1] or mode == "OUT" or (self.selControl and self.selControl ~= drop) then
tooltip:Clear()
elseif tooltip:CheckForUpdate(modList) then
if value.modId or #modList == 1 then
local mod = self.displayItem.affixes[value.modId or modList[1]]
tooltip:AddLine(16, "^7Affix: "..mod.affix)
for _, line in ipairs(mod) do
tooltip:AddLine(14, "^7"..line)
end
if mod.level > 1 then
tooltip:AddLine(16, "Level: "..mod.level)
end
if mod.modTags and #mod.modTags > 0 then
tooltip:AddLine(16, "Tags: "..table.concat(mod.modTags, ', '))
end
else
tooltip:AddLine(16, "^7"..#modList.." Tiers")
local minMod = self.displayItem.affixes[modList[1]]
local maxMod = self.displayItem.affixes[modList[#modList]]
for l, line in ipairs(minMod) do
local minLine = line:gsub("%((%d[%d%.]*)%-(%d[%d%.]*)%)", "%1")
local maxLine = maxMod[l]:gsub("%((%d[%d%.]*)%-(%d[%d%.]*)%)", "%2")
if maxLine == maxMod[l] then
tooltip:AddLine(14, maxLine)
else
local start = 1
tooltip:AddLine(14, (minLine:gsub("%d[%d%.]*", function(min)
local s, e, max = maxLine:find("(%d[%d%.]*)", start)
start = e + 1
if min == max then
return min
else
return "("..min.."-"..max..")"
end
end)))
end
end
tooltip:AddLine(16, "Level: "..minMod.level.." to "..maxMod.level)
-- Assuming that all mods have the same tags
if maxMod.modTags and #maxMod.modTags > 0 then
tooltip:AddLine(16, "Tags: "..table.concat(maxMod.modTags, ', '))
end
end
local mod = self.displayItem.affixes[value.modId or modList[1]]
local notableName = mod[1] and mod[1]:match("1 Added Passive Skill is (.*)")
local node = notableName and self.build.spec.tree.clusterNodeMap[notableName]
if node then
tooltip:AddSeparator(14)
-- Node name
self.socketViewer:AddNodeName(tooltip, node, self.build)
-- Node description
if node.sd[1] then
tooltip:AddLine(16, "")
for i, line in ipairs(node.sd) do
if line ~= " " and (node.mods[i].extra or not node.mods[i].list) then
local line = colorCodes.UNSUPPORTED .. line
line = main.notSupportedModTooltips and (line .. main.notSupportedTooltipText) or line
tooltip:AddLine(16, line)
else
tooltip:AddLine(16, colorCodes.MAGIC..line)
end
end
end
-- Reminder text
if node.reminderText then
tooltip:AddSeparator(14)
for _, line in ipairs(node.reminderText) do
tooltip:AddLine(14, "^xA0A080"..line)
end
end
-- Comparison
tooltip:AddSeparator(14)
self:AppendAddedNotableTooltip(tooltip, node)
-- Information of for this notable appears
local clusterInfo = self.build.data.clusterJewelInfoForNotable[notableName]
if clusterInfo then
tooltip:AddSeparator(14)
tooltip:AddLine(20, "^7"..notableName.." can appear on:")
local isFirstSize = true
for size, v in pairs(clusterInfo.size) do
tooltip:AddLine(18, colorCodes.MAGIC..size..":")
local sizeSkills = self.build.data.clusterJewels.jewels[size].skills
for i, type in ipairs(clusterInfo.jewelTypes) do
if sizeSkills[type] then
tooltip:AddLine(14, "^7 "..sizeSkills[type].name)
end
end
if not isFirstSize then
tooltip:AddLine(10, "")
end
isFirstSize = false
end
end
else
local mod = { }
if value.modId or #modList == 1 then
mod = self.displayItem.affixes[value.modId or modList[1]]
else
mod = self.displayItem.affixes[modList[1 + round((#modList - 1) * main.defaultItemAffixQuality)]]
end
-- Adding Mod
self:AddModComparisonTooltip(tooltip, mod)
end
end
end
drop.shown = function()
return self.displayItem and self.displayItem.crafted and i <= self.displayItem.affixLimit
end
slider = new("SliderControl", {"TOPLEFT",drop,"BOTTOMLEFT"}, {0, 2, 300, 16}, function(val)
local affix = self.displayItem[drop.outputTable][drop.outputIndex]
local index, range = slider:GetDivVal()
affix.modId = drop.list[drop.selIndex].modList[index]
affix.range = verifyRange(range, index, drop)
self.displayItem:Craft()
self:UpdateDisplayItemTooltip()
end)
slider.width = function()
return slider.divCount and 300 or 100
end
slider.tooltipFunc = function(tooltip, val)
local modList = drop.list[drop.selIndex].modList
if not modList or main.popups[1] or (self.selControl and self.selControl ~= slider) then
tooltip:Clear()
elseif tooltip:CheckForUpdate(val, modList) then
local index, range = slider:GetDivVal(val)
local modId = modList[index]
local mod = self.displayItem.affixes[modId]
for _, line in ipairs(mod) do
tooltip:AddLine(16, itemLib.applyRange(line, range))
end
tooltip:AddSeparator(10)
if #modList > 1 then
tooltip:AddLine(16, "^7Affix: Tier "..(#modList - isValueInArray(modList, modId) + 1).." ("..mod.affix..")")
else
tooltip:AddLine(16, "^7Affix: "..mod.affix)
end
for _, line in ipairs(mod) do
tooltip:AddLine(14, line)
end
if mod.level > 1 then
tooltip:AddLine(16, "Level: "..mod.level)
end
end
end
drop.slider = slider
self.controls["displayItemAffix"..i] = drop
self.controls["displayItemAffixLabel"..i] = new("LabelControl", {"RIGHT",drop,"LEFT"}, {-4, 0, 0, 14}, function()
return drop.outputTable == "prefixes" and "^7Prefix:" or "^7Suffix:"
end)
self.controls["displayItemAffixRange"..i] = slider
self.controls["displayItemAffixRangeLabel"..i] = new("LabelControl", {"RIGHT",slider,"LEFT"}, {-4, 0, 0, 14}, function()
return drop.selIndex > 1 and "^7Roll:" or "^x7F7F7FRoll:"
end)
end
-- Section: Custom modifiers
-- if either Custom or Crucible mod buttons are shown, create the control for the list of mods
self.controls.displayItemSectionCustom = new("Control", {"TOPLEFT",self.controls.displayItemSectionAffix,"BOTTOMLEFT"}, {0, 0, 0, function()
return (self.controls.displayItemAddCustom:IsShown() or self.controls.displayItemAddCrucible:IsShown()) and 28 + self.displayItem.customCount * 22 or 0
end})
self.controls.displayItemAddCustom = new("ButtonControl", {"TOPLEFT",self.controls.displayItemSectionCustom,"TOPLEFT"}, {0, 0, 120, 20}, "Add modifier...", function()
self:AddCustomModifierToDisplayItem()
end)
self.controls.displayItemAddCustom.shown = function()
return self.displayItem and (self.displayItem.rarity == "MAGIC" or self.displayItem.rarity == "RARE")
end
-- Section: Crucible modifiers
-- if the Add modifier button is not shown, take its place, otherwise move it to the right of it
self.controls.displayItemAddCrucible = new("ButtonControl", {"TOPLEFT",self.controls.displayItemSectionCustom,"TOPLEFT"}, {function()
return (self.controls.displayItemAddCustom:IsShown() and 128) or 0
end, 0, 150, 20}, "Add Crucible mod...", function()
self:AddCrucibleModifierToDisplayItem()
end)
self.controls.displayItemAddCrucible.shown = function()
return self.displayItem and (self.displayItem:GetPrimarySlot() == "Weapon 1" or self.displayItem.type == "Shield" or self.displayItem.canHaveShieldCrucibleTree)
end
-- Section: Modifier Range
self.controls.displayItemSectionRange = new("Control", {"TOPLEFT",self.controls.displayItemSectionCustom,"BOTTOMLEFT"}, {0, 0, 0, function()
return self.displayItem.rangeLineList[1] and 28 or 0
end})
self.controls.displayItemRangeLine = new("DropDownControl", {"TOPLEFT",self.controls.displayItemSectionRange,"TOPLEFT"}, {0, 0, 350, 18}, nil, function(index, value)
self.controls.displayItemRangeSlider.val = self.displayItem.rangeLineList[index].range
end)
self.controls.displayItemRangeLine.shown = function()
return self.displayItem and self.displayItem.rangeLineList[1] ~= nil
end
self.controls.displayItemRangeSlider = new("SliderControl", {"LEFT",self.controls.displayItemRangeLine,"RIGHT"}, {8, 0, 100, 18}, function(val)
self.displayItem.rangeLineList[self.controls.displayItemRangeLine.selIndex].range = val
self.displayItem:BuildAndParseRaw()
self:UpdateDisplayItemTooltip()
self:UpdateCustomControls()
end)
-- Tooltip anchor
self.controls.displayItemTooltipAnchor = new("Control", {"TOPLEFT",self.controls.displayItemSectionRange,"BOTTOMLEFT"})
-- Scroll bars
self.controls.scrollBarH = new("ScrollBarControl", nil, {0, 0, 0, 18}, 100, "HORIZONTAL", true)
self.controls.scrollBarV = new("ScrollBarControl", nil, {0, 0, 18, 0}, 100, "VERTICAL", true)
-- Initialise drag target lists
t_insert(self.controls.itemList.dragTargetList, self.controls.sharedItemList)
t_insert(self.controls.itemList.dragTargetList, build.controls.mainSkillMinion)
t_insert(self.controls.uniqueDB.dragTargetList, self.controls.itemList)
t_insert(self.controls.uniqueDB.dragTargetList, self.controls.sharedItemList)
t_insert(self.controls.uniqueDB.dragTargetList, build.controls.mainSkillMinion)
t_insert(self.controls.rareDB.dragTargetList, self.controls.itemList)
t_insert(self.controls.rareDB.dragTargetList, self.controls.sharedItemList)
t_insert(self.controls.rareDB.dragTargetList, build.controls.mainSkillMinion)
t_insert(self.controls.sharedItemList.dragTargetList, self.controls.itemList)
t_insert(self.controls.sharedItemList.dragTargetList, build.controls.mainSkillMinion)
for _, slot in pairs(self.slots) do
t_insert(self.controls.itemList.dragTargetList, slot)
t_insert(self.controls.uniqueDB.dragTargetList, slot)
t_insert(self.controls.rareDB.dragTargetList, slot)
t_insert(self.controls.sharedItemList.dragTargetList, slot)
end
-- Initialise item sets
self.itemSets = { }
self.itemSetOrderList = { 1 }
self:NewItemSet(1)
self:SetActiveItemSet(1)
self:PopulateSlots()
self.lastSlot = self.slots[baseSlots[#baseSlots]]
end)
function ItemsTabClass:Load(xml, dbFileName)
self.activeItemSetId = 0
self.itemSets = { }
self.itemSetOrderList = { }
self.tradeQuery.statSortSelectionList = { }
for _, node in ipairs(xml) do
if node.elem == "Item" then
local item = new("Item", "")
item.id = tonumber(node.attrib.id)
item.variant = tonumber(node.attrib.variant)
if node.attrib.variantAlt then
item.hasAltVariant = true
item.variantAlt = tonumber(node.attrib.variantAlt)
end
if node.attrib.variantAlt2 then
item.hasAltVariant2 = true
item.variantAlt2 = tonumber(node.attrib.variantAlt2)
end
if node.attrib.variantAlt3 then
item.hasAltVariant3 = true
item.variantAlt3 = tonumber(node.attrib.variantAlt3)
end
if node.attrib.variantAlt4 then
item.hasAltVariant4 = true
item.variantAlt4 = tonumber(node.attrib.variantAlt4)
end
if node.attrib.variantAlt5 then
item.hasAltVariant5 = true
item.variantAlt5 = tonumber(node.attrib.variantAlt5)
end
for _, child in ipairs(node) do
if type(child) == "string" then
item:ParseRaw(child)
elseif child.elem == "ModRange" then
local id = tonumber(child.attrib.id) or 0
local range = tonumber(child.attrib.range) or 1
-- This is garbage, but needed due to change to separate mod line lists
-- 'ModRange' elements are legacy though, so is this actually needed? :<
-- Maybe it is? Maybe it isn't? Maybe up is down? Maybe good is bad? AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-- Sorry, cluster jewels are making me crazy(-ier)
for _, list in ipairs{item.buffModLines, item.enchantModLines, item.scourgeModLines, item.implicitModLines, item.explicitModLines, item.crucibleModLines} do
if id <= #list then
list[id].range = range
break
end
id = id - #list
end
end
end
if item.base then
item:BuildModList()
self.items[item.id] = item
t_insert(self.itemOrderList, item.id)
end
-- Below is OBE and left for legacy compatibility (all Slots are part of ItemSets now)
elseif node.elem == "Slot" then
local slot = self.slots[node.attrib.name or ""]
if slot then
slot.selItemId = tonumber(node.attrib.itemId)
if slot.controls.activate then
slot.active = node.attrib.active == "true"
slot.controls.activate.state = slot.active
end
end
elseif node.elem == "ItemSet" then
local itemSet = self:NewItemSet(tonumber(node.attrib.id))
itemSet.title = node.attrib.title
itemSet.useSecondWeaponSet = node.attrib.useSecondWeaponSet == "true"
for _, child in ipairs(node) do
if child.elem == "Slot" then
local slotName = child.attrib.name or ""
if itemSet[slotName] then
itemSet[slotName].selItemId = tonumber(child.attrib.itemId)
itemSet[slotName].active = child.attrib.active == "true"
itemSet[slotName].pbURL = child.attrib.itemPbURL or ""
end
elseif child.elem == "SocketIdURL" then
local id = tonumber(child.attrib.nodeId)
itemSet[id] = { pbURL = child.attrib.itemPbURL or "" }
end
end
t_insert(self.itemSetOrderList, itemSet.id)
elseif node.elem == "TradeSearchWeights" then
for _, child in ipairs(node) do
local statSort = {
label = child.attrib.label,
stat = child.attrib.stat,
weightMult = tonumber(child.attrib.weightMult)
}
t_insert(self.tradeQuery.statSortSelectionList, statSort)
end
end
end
if not self.itemSetOrderList[1] then
self.activeItemSet = self:NewItemSet(1)
self.activeItemSet.useSecondWeaponSet = xml.attrib.useSecondWeaponSet == "true"
self.itemSetOrderList[1] = 1
end
self:SetActiveItemSet(tonumber(xml.attrib.activeItemSet) or 1)
if xml.attrib.showStatDifferences then
self.showStatDifferences = xml.attrib.showStatDifferences == "true"
end
self:ResetUndo()
end
function ItemsTabClass:Save(xml)
xml.attrib = {
activeItemSet = tostring(self.activeItemSetId),
useSecondWeaponSet = tostring(self.activeItemSet.useSecondWeaponSet),
showStatDifferences = tostring(self.showStatDifferences),
}
for _, id in ipairs(self.itemOrderList) do
local item = self.items[id]
local child = {
elem = "Item",
attrib = {
id = tostring(id),
variant = item.variant and tostring(item.variant),
variantAlt = item.variantAlt and tostring(item.variantAlt),
variantAlt2 = item.variantAlt2 and tostring(item.variantAlt2),
variantAlt3 = item.variantAlt3 and tostring(item.variantAlt3),
variantAlt4 = item.variantAlt4 and tostring(item.variantAlt4),
variantAlt5 = item.variantAlt5 and tostring(item.variantAlt5)
}
}
item:BuildAndParseRaw()
t_insert(child, item.raw)
local id = #item.buffModLines + 1
for _, modLine in ipairs(item.enchantModLines) do
if modLine.range then
t_insert(child, { elem = "ModRange", attrib = { id = tostring(id), range = tostring(modLine.range) } })
end
id = id + 1
end
for _, modLine in ipairs(item.scourgeModLines) do
if modLine.range then
t_insert(child, { elem = "ModRange", attrib = { id = tostring(id), range = tostring(modLine.range) } })
end
id = id + 1
end
for _, modLine in ipairs(item.implicitModLines) do
if modLine.range then
t_insert(child, { elem = "ModRange", attrib = { id = tostring(id), range = tostring(modLine.range) } })
end
id = id + 1
end
for _, modLine in ipairs(item.explicitModLines) do
if modLine.range then
t_insert(child, { elem = "ModRange", attrib = { id = tostring(id), range = tostring(modLine.range) } })
end
id = id + 1
end
for _, modLine in ipairs(item.crucibleModLines) do
if modLine.range then
t_insert(child, { elem = "ModRange", attrib = { id = tostring(id), range = tostring(modLine.range) } })
end
id = id + 1
end
t_insert(xml, child)
end
for _, itemSetId in ipairs(self.itemSetOrderList) do
local itemSet = self.itemSets[itemSetId]
local child = { elem = "ItemSet", attrib = { id = tostring(itemSetId), title = itemSet.title, useSecondWeaponSet = tostring(itemSet.useSecondWeaponSet) } }
for slotName, slot in pairs(self.slots) do
if not slot.nodeId then
t_insert(child, { elem = "Slot", attrib = { name = slotName, itemId = tostring(itemSet[slotName].selItemId), itemPbURL = itemSet[slotName].pbURL or "", active = itemSet[slotName].active and "true" }})
else
if self.build.spec.allocNodes[slot.nodeId] then
t_insert(child, { elem = "SocketIdURL", attrib = { name = slotName, nodeId = tostring(slot.nodeId), itemPbURL = itemSet[slot.nodeId] and itemSet[slot.nodeId].pbURL or ""}})
end
end
end
t_insert(xml, child)
end
if self.tradeQuery.statSortSelectionList then
local parent = {
elem = "TradeSearchWeights"
}
for _, statSort in ipairs(self.tradeQuery.statSortSelectionList) do
if statSort.weightMult and statSort.weightMult > 0 then
local child = {
elem = "Stat",
attrib = {
label = statSort.label,
stat = statSort.stat,
weightMult = s_format("%.2f", tostring(statSort.weightMult))
}
}
t_insert(parent, child)
end
end
t_insert(xml, parent)
end
end
function ItemsTabClass:Draw(viewPort, inputEvents)
self.x = viewPort.x
self.y = viewPort.y
self.width = viewPort.width
self.height = viewPort.height
self.controls.scrollBarH.width = viewPort.width
self.controls.scrollBarH.x = viewPort.x
self.controls.scrollBarH.y = viewPort.y + viewPort.height - 18
self.controls.scrollBarV.height = viewPort.height - 18
self.controls.scrollBarV.x = viewPort.x + viewPort.width - 18
self.controls.scrollBarV.y = viewPort.y
do
local maxY = select(2, self.lastSlot:GetPos()) + 24
local maxX = self.anchorDisplayItem:GetPos() + 462
if self.displayItem then
local x, y = self.controls.displayItemTooltipAnchor:GetPos()
local ttW, ttH = self.displayItemTooltip:GetDynamicSize(viewPort)
maxY = m_max(maxY, y + ttH + 4)
maxX = m_max(maxX, x + ttW + 80)
end
local contentHeight = maxY - self.y
local contentWidth = maxX - self.x
local v = contentHeight > viewPort.height
local h = contentWidth > viewPort.width - (v and 20 or 0)
if h then
v = contentHeight > viewPort.height - 20
end
self.controls.scrollBarV:SetContentDimension(contentHeight, viewPort.height - (h and 20 or 0))
self.controls.scrollBarH:SetContentDimension(contentWidth, viewPort.width - (v and 20 or 0))
if self.snapHScroll == "RIGHT" then
self.controls.scrollBarH:SetOffset(self.controls.scrollBarH.offsetMax)
elseif self.snapHScroll == "LEFT" then
self.controls.scrollBarH:SetOffset(0)
end
self.snapHScroll = nil
self.maxY = h and self.controls.scrollBarH.y or viewPort.y + viewPort.height
end
self.x = self.x - self.controls.scrollBarH.offset
self.y = self.y - self.controls.scrollBarV.offset
for _, event in ipairs(inputEvents) do
if event.type == "KeyDown" then
if event.key == "v" and IsKeyDown("CTRL") then
local newItem = Paste()
if newItem:find("{ ", 0, true) then
main:OpenConfirmPopup("Warning", "\"Advanced Item Descriptions\" (Ctrl+Alt+c) are unsupported.\n\nAbort paste?", "OK", function()
self:SetDisplayItem()
end)
end
if newItem then
self:CreateDisplayItemFromRaw(newItem, true)
end
elseif event.key == "e" then
local mOverControl = self:GetMouseOverControl()
if mOverControl and mOverControl._className == "ItemSlotControl" and mOverControl.selItemId ~= 0 then
-- Trigger itemList's double click procedure
self.controls.itemList:OnSelClick(0, mOverControl.selItemId, true)
end
elseif event.key == "z" and IsKeyDown("CTRL") then
self:Undo()
self.build.buildFlag = true
elseif event.key == "y" and IsKeyDown("CTRL") then
self:Redo()
self.build.buildFlag = true
elseif event.key == "f" and IsKeyDown("CTRL") then
local selUnique = self.selControl == self.controls.uniqueDB.controls.search
local selRare = self.selControl == self.controls.rareDB.controls.search
if selUnique or (self.controls.selectDB:IsShown() and not selRare and self.controls.selectDB.selIndex == 2) then
self:SelectControl(self.controls.rareDB.controls.search)
self.controls.selectDB.selIndex = 2
else
self:SelectControl(self.controls.uniqueDB.controls.search)
self.controls.selectDB.selIndex = 1
end
elseif event.key == "d" and IsKeyDown("CTRL") then
self.showStatDifferences = not self.showStatDifferences
self.build.buildFlag = true
end
end
end
self:ProcessControlsInput(inputEvents, viewPort)
for _, event in ipairs(inputEvents) do
if event.type == "KeyUp" then
if self.controls.scrollBarV:IsScrollDownKey(event.key) then
if self.controls.scrollBarV:IsMouseOver() or not self.controls.scrollBarH:IsShown() then
self.controls.scrollBarV:Scroll(1)
else
self.controls.scrollBarH:Scroll(1)
end
elseif self.controls.scrollBarV:IsScrollUpKey(event.key) then
if self.controls.scrollBarV:IsMouseOver() or not self.controls.scrollBarH:IsShown() then
self.controls.scrollBarV:Scroll(-1)
else
self.controls.scrollBarH:Scroll(-1)
end
end
end
end
main:DrawBackground(viewPort)
local newItemList = { }
for index, itemSetId in ipairs(self.itemSetOrderList) do
local itemSet = self.itemSets[itemSetId]
t_insert(newItemList, itemSet.title or "Default")
if itemSetId == self.activeItemSetId then
self.controls.setSelect.selIndex = index
end
end
self.controls.setSelect:SetList(newItemList)
if self.displayItem then
local x, y = self.controls.displayItemTooltipAnchor:GetPos()
self.displayItemTooltip:Draw(x, y, nil, nil, viewPort)
end
self:UpdateSockets()
self:DrawControls(viewPort)
if self.controls.scrollBarH:IsShown() then
self.controls.scrollBarH:Draw(viewPort)
end
if self.controls.scrollBarV:IsShown() then
self.controls.scrollBarV:Draw(viewPort)
end
self.controls.specSelect:SetList(self.build.treeTab:GetSpecList())
end
-- Creates a new item set
function ItemsTabClass:NewItemSet(itemSetId)
local itemSet = { id = itemSetId }
if not itemSetId then
itemSet.id = 1
while self.itemSets[itemSet.id] do
itemSet.id = itemSet.id + 1
end
end
for slotName, slot in pairs(self.slots) do
if not slot.nodeId then
itemSet[slotName] = { selItemId = 0 }
end
end
self.itemSets[itemSet.id] = itemSet
return itemSet
end
-- Changes the active item set
function ItemsTabClass:SetActiveItemSet(itemSetId)
local prevSet = self.activeItemSet
if not self.itemSets[itemSetId] then
itemSetId = self.itemSetOrderList[1]
end
self.activeItemSetId = itemSetId
self.activeItemSet = self.itemSets[itemSetId]
local curSet = self.activeItemSet
for slotName, slot in pairs(self.slots) do
if not slot.nodeId then
if prevSet then
-- Update the previous set
prevSet[slotName].selItemId = slot.selItemId
prevSet[slotName].active = slot.active
end
-- Equip the incoming set's item
slot.selItemId = curSet[slotName].selItemId
slot.active = curSet[slotName].active
if slot.controls.activate then
slot.controls.activate.state = slot.active
end
end
end
self.build.buildFlag = true
self:PopulateSlots()
self.build:SyncLoadouts()
end
-- Equips the given item in the given item set
function ItemsTabClass:EquipItemInSet(item, itemSetId)
local itemSet = self.itemSets[itemSetId]
local slotName = item:GetPrimarySlot()
if self.slots[slotName].weaponSet == 1 and itemSet.useSecondWeaponSet then
-- Redirect to second weapon set
slotName = slotName .. " Swap"
end
if not item.id or not self.items[item.id] then
item = new("Item", item.raw)
self:AddItem(item, true)
end
local altSlot = slotName:gsub("1","2")
if IsKeyDown("SHIFT") then
-- Redirect to second slot if possible
if self:IsItemValidForSlot(item, altSlot, itemSet) then
slotName = altSlot
end
end
if itemSet == self.activeItemSet then
self.slots[slotName]:SetSelItemId(item.id)
else
itemSet[slotName].selItemId = item.id
if itemSet[altSlot].selItemId ~= 0 and not self:IsItemValidForSlot(self.items[itemSet[altSlot].selItemId], altSlot, itemSet) then
itemSet[altSlot].selItemId = 0
end
end
self:PopulateSlots()
self:AddUndoState()
self.build.buildFlag = true
end
-- Update the item lists for all the slot controls
function ItemsTabClass:PopulateSlots()
for _, slot in pairs(self.slots) do
slot:Populate()
end
end
-- Updates the status and position of the socket controls
function ItemsTabClass:UpdateSockets()
-- Build a list of active sockets
local activeSocketList = { }
for nodeId, slot in pairs(self.sockets) do
if self.build.spec.allocNodes[nodeId] then
t_insert(activeSocketList, nodeId)
slot.inactive = false
else
slot.inactive = true
end
end
table.sort(activeSocketList)
-- Update the state of the active socket controls
self.lastSlot = self.slots[baseSlots[#baseSlots]]
for index, nodeId in ipairs(activeSocketList) do
self.sockets[nodeId].label = "Socket #"..index
self.lastSlot = self.sockets[nodeId]
end
if main.portraitMode then
self.controls.itemList:SetAnchor("TOPRIGHT",self.lastSlot,"BOTTOMRIGHT", 0, 40)
end
end
-- Returns the slot control and equipped jewel for the given node ID
function ItemsTabClass:GetSocketAndJewelForNodeID(nodeId)
return self.sockets[nodeId], self.items[self.sockets[nodeId].selItemId]
end
-- Adds the given item to the build's item list
function ItemsTabClass:AddItem(item, noAutoEquip, index)
if not item.id then
-- Find an unused item ID
item.id = 1
while self.items[item.id] do
item.id = item.id + 1
end
if index then
t_insert(self.itemOrderList, index, item.id)
else
-- Add it to the end of the display order list
t_insert(self.itemOrderList, item.id)
end
if not noAutoEquip then
-- Autoequip it
for _, slot in ipairs(self.orderedSlots) do
if not slot.nodeId and slot.selItemId == 0 and slot:IsShown() and self:IsItemValidForSlot(item, slot.slotName) then
slot:SetSelItemId(item.id)
break
end
end
end
end
-- Add it to the list
local replacing = self.items[item.id]
self.items[item.id] = item
item:BuildModList()
if replacing and (replacing.clusterJewel or item.clusterJewel or replacing.baseName == "Timeless Jewel") then
-- We're replacing an existing item, and either the new or old one is a cluster jewel
if isValueInTable(self.build.spec.jewels, item.id) then
-- Item is currently equipped, so we need to rebuild the graphs
self.build.spec:BuildClusterJewelGraphs()
end
end
end
-- Adds the current display item to the build's item list
function ItemsTabClass:AddDisplayItem(noAutoEquip)
-- Add it to the list and clear the current display item
self:AddItem(self.displayItem, noAutoEquip)
self:SetDisplayItem()
self:PopulateSlots()
self:AddUndoState()
self.build.buildFlag = true
end
-- Sorts the build's item list
function ItemsTabClass:SortItemList()
table.sort(self.itemOrderList, function(a, b)
local itemA = self.items[a]
local itemB = self.items[b]
local primSlotA = itemA:GetPrimarySlot()
local primSlotB = itemB:GetPrimarySlot()
if primSlotA ~= primSlotB then
if not self.slotOrder[primSlotA] then
return false
elseif not self.slotOrder[primSlotB] then
return true
end
return self.slotOrder[primSlotA] < self.slotOrder[primSlotB]
end
local equipSlotA, equipSetA = self:GetEquippedSlotForItem(itemA)
local equipSlotB, equipSetB = self:GetEquippedSlotForItem(itemB)
if equipSlotA and equipSlotB then
if equipSlotA ~= equipSlotB then
return self.slotOrder[equipSlotA.slotName] < self.slotOrder[equipSlotB.slotName]
elseif equipSetA and not equipSetB then
return false
elseif not equipSetA and equipSetB then
return true
elseif equipSetA and equipSetB then
return isValueInArray(self.itemSetOrderList, equipSetA.id) < isValueInArray(self.itemSetOrderList, equipSetB.id)
end
elseif equipSlotA then
return true
elseif equipSlotB then
return false
end
return itemA.name < itemB.name
end)
self:AddUndoState()
end
-- Deletes an item
function ItemsTabClass:DeleteItem(item, deferUndoState)
for slotName, slot in pairs(self.slots) do
if slot.selItemId == item.id then
slot:SetSelItemId(0)
self.build.buildFlag = true
end
if not slot.nodeId then
for _, itemSet in pairs(self.itemSets) do
if itemSet[slotName].selItemId == item.id then
itemSet[slotName].selItemId = 0
self.build.buildFlag = true
end
end
end
end
for index, id in pairs(self.itemOrderList) do
if id == item.id then
t_remove(self.itemOrderList, index)
break
end
end
for _, spec in pairs(self.build.treeTab.specList) do
local rebuildClusterJewelGraphs = false
for nodeId, itemId in pairs(spec.jewels) do
if itemId == item.id then
spec.jewels[nodeId] = 0
rebuildClusterJewelGraphs = true
-- Deallocate all nodes that required this jewel
if spec.nodes[nodeId] then
for depNodeId, depNode in ipairs(spec.nodes[nodeId].depends) do
depNode.alloc = false
spec.allocNodes[depNodeId] = nil
end
spec.nodes[nodeId].alloc = false
spec.allocNodes[nodeId] = nil
end
end
end
if rebuildClusterJewelGraphs and not deferUndoState then
spec:BuildClusterJewelGraphs()
end
end
self.items[item.id] = nil
if not deferUndoState then
self:PopulateSlots()
self:AddUndoState()
end
end
-- Attempt to create a new item from the given item raw text and sets it as the new display item
function ItemsTabClass:CreateDisplayItemFromRaw(itemRaw, normalise)
local newItem = new("Item", itemRaw)
if newItem.base then
if normalise then
newItem:NormaliseQuality()
newItem:BuildModList()
end
self:SetDisplayItem(newItem)
end
end
-- Sets the display item to the given item
function ItemsTabClass:SetDisplayItem(item)
self.displayItem = item
if item then
-- Update the display item controls
self:UpdateDisplayItemTooltip()
self.snapHScroll = "RIGHT"
self.controls.displayItemVariant.list = item.variantList
self.controls.displayItemVariant.selIndex = item.variant
self.controls.displayItemVariant:CheckDroppedWidth(true)
if item.hasAltVariant then
self.controls.displayItemAltVariant.list = item.variantList
self.controls.displayItemAltVariant.selIndex = item.variantAlt
self.controls.displayItemAltVariant:CheckDroppedWidth(true)
end
if item.hasAltVariant2 then
self.controls.displayItemAltVariant2.list = item.variantList
self.controls.displayItemAltVariant2.selIndex = item.variantAlt2
self.controls.displayItemAltVariant2:CheckDroppedWidth(true)
end
if item.hasAltVariant3 then
self.controls.displayItemAltVariant3.list = item.variantList
self.controls.displayItemAltVariant3.selIndex = item.variantAlt3
self.controls.displayItemAltVariant3:CheckDroppedWidth(true)
end
if item.hasAltVariant4 then
self.controls.displayItemAltVariant4.list = item.variantList
self.controls.displayItemAltVariant4.selIndex = item.variantAlt4
self.controls.displayItemAltVariant4:CheckDroppedWidth(true)
end
if item.hasAltVariant5 then
self.controls.displayItemAltVariant5.list = item.variantList
self.controls.displayItemAltVariant5.selIndex = item.variantAlt5
self.controls.displayItemAltVariant5:CheckDroppedWidth(true)
end
self:UpdateSocketControls()
if item.crafted then
self:UpdateAffixControls()
end
-- Set both influence dropdowns
local influence1 = 1
local influence2 = 1
local influenceDisplayList = { "Influence" }
for i, curInfluenceInfo in ipairs((item.canHaveEldritchInfluence or item.type == "Helmet" or item.type == "Body Armour" or item.type == "Gloves" or item.type == "Boots") and itemLib.influenceInfo.all or itemLib.influenceInfo.default) do
influenceDisplayList[i + 1] = curInfluenceInfo.display
end
self.controls.displayItemInfluence.list = influenceDisplayList
self.controls.displayItemInfluence2.list = influenceDisplayList
for i, curInfluenceInfo in ipairs(influenceInfo) do
if item[curInfluenceInfo.key] then
if influence1 == 1 then
influence1 = i + 1
elseif influence2 == 1 then
influence2 = i + 1
break
end
end
end
self.controls.displayItemInfluence:SetSel(influence1, true) -- Don't call the selection function for the first influence dropdown as the second dropdown isn't properly set yet.
self.controls.displayItemInfluence2:SetSel(influence2) -- The selection function for the second dropdown properly handles everything for both dropdowns
self.controls.displayItemQualityEdit:SetText(item.quality)
self.controls.displayItemCatalyst:SetSel((item.catalyst or 0) + 1)
if item.catalystQuality then
self.controls.displayItemCatalystQualityEdit:SetText(m_max(item.catalystQuality, 0))
else
self.controls.displayItemCatalystQualityEdit:SetText(0)
end
self:UpdateCustomControls()
self:UpdateDisplayItemRangeLines()
if item.clusterJewel and item.crafted then
self:UpdateClusterJewelControls()
end
else
self.snapHScroll = "LEFT"
end
end
function ItemsTabClass:UpdateDisplayItemTooltip()
self.displayItemTooltip:Clear()
self:AddItemTooltip(self.displayItemTooltip, self.displayItem)
self.displayItemTooltip.center = true
end
function ItemsTabClass:UpdateSocketControls()
local sockets = self.displayItem.sockets
for i = 1, #sockets - self.displayItem.abyssalSocketCount do
self.controls["displayItemSocket"..i]:SelByValue(sockets[i].color, "color")
if i > 1 then
self.controls["displayItemLink"..(i-1)].state = sockets[i].group == sockets[i-1].group
end
end
end
function ItemsTabClass:UpdateClusterJewelControls()
local item = self.displayItem
local unavailableSkills = { ["affliction_strength"] = true, ["affliction_dexterity"] = true, ["affliction_intelligence"] = true, }
-- Update list of skills
local skillList = wipeTable(self.controls.displayItemClusterJewelSkill.list)
for skillId, skill in pairs(item.clusterJewel.skills) do
if not unavailableSkills[skillId] then
t_insert(skillList, { label = skill.name, skillId = skillId })
end
end
table.sort(skillList, function(a, b) return a.label < b.label end)
if not item.clusterJewelSkill or not item.clusterJewel.skills[item.clusterJewelSkill] then
item.clusterJewelSkill = skillList[1].skillId
end
self.controls.displayItemClusterJewelSkill:SelByValue(item.clusterJewelSkill, "skillId")
-- Update added node count slider
local countControl = self.controls.displayItemClusterJewelNodeCount
item.clusterJewelNodeCount = m_min(m_max(item.clusterJewelNodeCount or item.clusterJewel.maxNodes, item.clusterJewel.minNodes), item.clusterJewel.maxNodes)
countControl.divCount = item.clusterJewel.maxNodes - item.clusterJewel.minNodes
countControl.val = (item.clusterJewelNodeCount - item.clusterJewel.minNodes) / (item.clusterJewel.maxNodes - item.clusterJewel.minNodes)
self:CraftClusterJewel()
end
function ItemsTabClass:CraftClusterJewel()
local item = self.displayItem
wipeTable(item.enchantModLines)
t_insert(item.enchantModLines, { line = "Adds "..(item.clusterJewelNodeCount or item.clusterJewel.maxNodes).." Passive Skills", crafted = true })
if item.clusterJewel.size == "Large" then
t_insert(item.enchantModLines, { line = "2 Added Passive Skills are Jewel Sockets", crafted = true })
elseif item.clusterJewel.size == "Medium" then
t_insert(item.enchantModLines, { line = "1 Added Passive Skill is a Jewel Socket", crafted = true })
end
local skill = item.clusterJewel.skills[item.clusterJewelSkill]
t_insert(item.enchantModLines, { line = table.concat(skill.enchant, "\n"), crafted = true })
item:BuildAndParseRaw()
-- Update affixes manually to force out affixes that may now be invalid
self:UpdateAffixControls()
for i = 1, item.affixLimit do
local drop = self.controls["displayItemAffix"..i]
drop.selFunc(drop.selIndex, drop.list[drop.selIndex])
end
end
-- Update affix selection controls
function ItemsTabClass:UpdateAffixControls()
local item = self.displayItem
local prefixLimit = item.prefixes.limit or (item.affixLimit / 2)
for i = 1, item.affixLimit do
if i <= prefixLimit then
self:UpdateAffixControl(self.controls["displayItemAffix"..i], item, "Prefix", "prefixes", i)
else
self:UpdateAffixControl(self.controls["displayItemAffix"..i], item, "Suffix", "suffixes", i - prefixLimit)
end
end
-- The custom affixes may have had their indexes changed, so the custom control UI is also rebuilt so that it will
-- reference the correct affix index.
self:UpdateCustomControls()
end
function ItemsTabClass:UpdateAffixControl(control, item, type, outputTable, outputIndex)
local extraTags = { }
local excludeGroups = { }
for _, table in ipairs({"prefixes","suffixes"}) do
for index = 1, (item[table].limit or (item.affixLimit / 2)) do
if index ~= outputIndex or table ~= outputTable then
local mod = item.affixes[item[table][index] and item[table][index].modId]
if mod then
if mod.group then
excludeGroups[mod.group] = true
end
if mod.tags then
for _, tag in ipairs(mod.tags) do
extraTags[tag] = true
end
end
end
end
end
end
if item.clusterJewel and item.clusterJewelSkill then
local skill = item.clusterJewel.skills[item.clusterJewelSkill]
if skill then
extraTags[skill.tag] = true
end
end
local affixList = { }
for modId, mod in pairs(item.affixes) do
if mod.type == type and not excludeGroups[mod.group] and item:GetModSpawnWeight(mod, extraTags) > 0 and not item:CheckIfModIsDelve(mod) then
t_insert(affixList, modId)
end
end
table.sort(affixList, function(a, b)
local modA = item.affixes[a]
local modB = item.affixes[b]
for i = 1, m_max(#modA, #modB) do
if not modA[i] then
return true
elseif not modB[i] then
return false
elseif modA.statOrder[i] ~= modB.statOrder[i] then
return modA.statOrder[i] < modB.statOrder[i]
end
end
return modA.level > modB.level
end)
control.selIndex = 1
control.list = { "None" }
control.outputTable = outputTable
control.outputIndex = outputIndex
control.slider.shown = false
control.slider.val = main.defaultItemAffixQuality or 0.5
local selAffix = item[outputTable][outputIndex].modId
if (item.type == "Jewel" and item.base.subType ~= "Abyss") then
for i, modId in pairs(affixList) do
local mod = item.affixes[modId]
if selAffix == modId then
control.selIndex = i + 1
end
local modString = table.concat(mod, "/")
local label = modString
if item.type == "Flask" then
label = mod.affix .. " ^8[" .. modString .. "]"
end
control.list[i + 1] = {
label = label,
modList = { modId },
modId = modId,
haveRange = modString:match("%(%-?[%d%.]+%-%-?[%d%.]+%)"),
}
end
else
local lastSeries
for _, modId in ipairs(affixList) do
local mod = item.affixes[modId]
if not lastSeries or not tableDeepEquals(lastSeries.statOrder, mod.statOrder) then
local modString = table.concat(mod, "/")
lastSeries = {
label = modString,
modList = { },
haveRange = modString:match("%(%-?[%d%.]+%-%-?[%d%.]+%)"),
statOrder = mod.statOrder,
}
t_insert(control.list, lastSeries)
end
if selAffix == modId then
control.selIndex = #control.list
end
t_insert(lastSeries.modList, 1, modId)
if #lastSeries.modList == 2 then
lastSeries.label = lastSeries.label:gsub("%(%-?[%d%.]+%-%-?[%d%.]+%)","#"):gsub("%-?%d+%.?%d*","#")
lastSeries.haveRange = true
end
end
end
if control.list[control.selIndex].haveRange then
control.slider.divCount = #control.list[control.selIndex].modList
control.slider.val = (isValueInArray(control.list[control.selIndex].modList, selAffix) - 1 + (item[outputTable][outputIndex].range or 0.5)) / control.slider.divCount
if control.slider.divCount == 1 then
control.slider.divCount = nil
end
control.slider.shown = true
end
end
-- Create/update custom modifier controls
function ItemsTabClass:UpdateCustomControls()
local item = self.displayItem
local i = 1
local modLines = copyTable(item.explicitModLines)
if item.crucibleModLines and #item.crucibleModLines > 0 then
for _, line in ipairs(item.crucibleModLines) do
t_insert(modLines, line)
end
end
if item.rarity == "MAGIC" or item.rarity == "RARE" or (item.crucibleModLines and #item.crucibleModLines > 0) then
for index, modLine in ipairs(modLines) do
if modLine.custom or modLine.crafted or modLine.crucible then
local line = itemLib.formatModLine(modLine)
if line then
if not self.controls["displayItemCustomModifierRemove"..i] then
self.controls["displayItemCustomModifierRemove"..i] = new("ButtonControl", {"TOPLEFT",self.controls.displayItemSectionCustom,"TOPLEFT"}, {0, i * 22 + 4, 70, 20}, "^7Remove")
self.controls["displayItemCustomModifier"..i] = new("LabelControl", {"LEFT",self.controls["displayItemCustomModifierRemove"..i],"RIGHT"}, {65, 0, 0, 16})
self.controls["displayItemCustomModifierLabel"..i] = new("LabelControl", {"LEFT",self.controls["displayItemCustomModifierRemove"..i],"RIGHT"}, {5, 0, 0, 16})
end
self.controls["displayItemCustomModifierRemove"..i].shown = true
local label = itemLib.formatModLine(modLine)
if DrawStringCursorIndex(16, "VAR", label, 330, 10) < #label then
label = label:sub(1, DrawStringCursorIndex(16, "VAR", label, 310, 10)) .. "..."
end
self.controls["displayItemCustomModifier"..i].label = label
self.controls["displayItemCustomModifierLabel"..i].label = modLine.crafted and " ^7Crafted:" or modLine.crucible and "^7Crucible:" or " ^7Custom:"
self.controls["displayItemCustomModifierRemove"..i].onClick = function()
if index > #item.explicitModLines then
t_remove(item.crucibleModLines, index - #item.explicitModLines)
else
t_remove(item.explicitModLines, index)
end
item:BuildAndParseRaw()
local id = item.id
self:CreateDisplayItemFromRaw(item:BuildRaw())
self.displayItem.id = id
end
i = i + 1
end
end
end
end
item.customCount = i - 1
while self.controls["displayItemCustomModifierRemove"..i] do
self.controls["displayItemCustomModifierRemove"..i].shown = false
i = i + 1
end
end
-- Updates the range line dropdown and range slider for the current display item
function ItemsTabClass:UpdateDisplayItemRangeLines()
if self.displayItem and self.displayItem.rangeLineList[1] then
wipeTable(self.controls.displayItemRangeLine.list)
for _, modLine in ipairs(self.displayItem.rangeLineList) do
t_insert(self.controls.displayItemRangeLine.list, modLine.line)
end
self.controls.displayItemRangeLine.selIndex = 1
self.controls.displayItemRangeSlider.val = self.displayItem.rangeLineList[1].range
end
end
local function checkLineForAllocates(line, nodes)
if nodes and string.match(line, "Allocates") then
local nodeId = tonumber(string.match(line, "%d+"))
if nodes[nodeId] then
return "Allocates "..nodes[nodeId].name
end
end
return line
end
function ItemsTabClass:AddModComparisonTooltip(tooltip, mod)
local slotName = self.displayItem:GetPrimarySlot()
local newItem = new("Item", self.displayItem:BuildRaw())
for _, subMod in ipairs(mod) do
t_insert(newItem.explicitModLines, { line = checkLineForAllocates(subMod, self.build.spec.nodes), modTags = mod.modTags, [mod.type] = true })
end
newItem:BuildAndParseRaw()
local calcFunc = self.build.calcsTab:GetMiscCalculator()
local outputBase = calcFunc({ repSlotName = slotName, repItem = self.displayItem })
local outputNew = calcFunc({ repSlotName = slotName, repItem = newItem })
self.build:AddStatComparesToTooltip(tooltip, outputBase, outputNew, "\nAdding this mod will give: ")
end
-- Returns the first slot in which the given item is equipped
function ItemsTabClass:GetEquippedSlotForItem(item)
for _, slot in ipairs(self.orderedSlots) do
if not slot.inactive then
if slot.selItemId == item.id then
return slot
end
for _, itemSetId in ipairs(self.itemSetOrderList) do
local itemSet = self.itemSets[itemSetId]
if itemSetId ~= self.activeItemSetId and itemSet[slot.slotName] and itemSet[slot.slotName].selItemId == item.id then
return slot, itemSet
end
end
end
end
end
-- Check if the given item could be equipped in the given slot, taking into account possible conflicts with currently equipped items
-- For example, a shield is not valid for Weapon 2 if Weapon 1 is a staff, and a wand is not valid for Weapon 2 if Weapon 1 is a dagger
function ItemsTabClass:IsItemValidForSlot(item, slotName, itemSet)
itemSet = itemSet or self.activeItemSet
local slotType, slotId = slotName:match("^([%a ]+) (%d+)$")
if not slotType then
slotType = slotName
end
if slotType == "Jewel" then
-- Special checks for jewel sockets
local node = self.build.spec.tree.nodes[tonumber(slotId)] or self.build.spec.nodes[tonumber(slotId)]
if not node or item.type ~= "Jewel" then
return false
elseif node.charmSocket or item.base.subType == "Charm" then
-- Charm sockets can only have charms, and charms can only be in charm sockets
if node.charmSocket and item.base.subType == "Charm" then
return true
end
return false
elseif item.clusterJewel and not node.expansionJewel then
-- Don't allow cluster jewels in inner sockets
return false
elseif not node.expansionJewel or node.expansionJewel.size == 2 then
-- Outer sockets can fit anything
return true
else
-- Only allow jewels that fit in this socket
return not item.clusterJewel or item.clusterJewel.sizeIndex <= node.expansionJewel.size
end
elseif item.type == slotType then
return true
elseif item.type == "Tincture" and slotType == "Flask" then
return true
elseif item.type == "Jewel" and item.base.subType == "Abyss" and slotName:match("Abyssal Socket") then
return true
elseif slotName == "Weapon 1" or slotName == "Weapon 1 Swap" or slotName == "Weapon" then
return item.base.weapon ~= nil
elseif slotName == "Weapon 2" or slotName == "Weapon 2 Swap" then
local weapon1Sel = itemSet[slotName == "Weapon 2" and "Weapon 1" or "Weapon 1 Swap"].selItemId or 0
local weapon1Type = self.items[weapon1Sel] and self.items[weapon1Sel].base.type or "None"
if weapon1Type == "None" then
return item.type == "Shield" or (self.build.data.weaponTypeInfo[item.type] and self.build.data.weaponTypeInfo[item.type].oneHand)
elseif weapon1Type == "Bow" then
return item.type == "Quiver"
elseif self.build.data.weaponTypeInfo[weapon1Type].oneHand then
return item.type == "Shield" or (self.build.data.weaponTypeInfo[item.type] and self.build.data.weaponTypeInfo[item.type].oneHand and ((weapon1Type == "Wand" and item.type == "Wand") or (weapon1Type ~= "Wand" and item.type ~= "Wand")))
end
end
end
-- Opens the item set manager
function ItemsTabClass:OpenItemSetManagePopup()
local controls = { }
controls.setList = new("ItemSetListControl", nil, {-155, 50, 300, 200}, self)
controls.sharedList = new("SharedItemSetListControl", nil, {155, 50, 300, 200}, self)
controls.setList.dragTargetList = { controls.sharedList }
controls.sharedList.dragTargetList = { controls.setList }
controls.close = new("ButtonControl", nil, {0, 260, 90, 20}, "Done", function()
main:ClosePopup()
end)
main:OpenPopup(630, 290, "Manage Item Sets", controls)
end
-- Opens the item crafting popup
function ItemsTabClass:CraftItem()
local controls = { }
local function makeItem(base)
local item = new("Item")
item.name = base.name
item.base = base.base
item.baseName = base.name
item.buffModLines = { }
item.enchantModLines = { }
item.classRequirementModLines = { }
item.scourgeModLines = { }
item.implicitModLines = { }
item.explicitModLines = { }
item.crucibleModLines = { }
if base.base.type == "Amulet" or base.base.type == "Belt" or base.base.type == "Jewel" or base.base.type == "Quiver" or base.base.type == "Ring" then
item.quality = nil
else
item.quality = 0
end
local raritySel = controls.rarity.selIndex
if base.base.flask
or (base.base.type == "Jewel" and base.base.subType == "Charm")
or base.base.type == "Tincture"
then
if raritySel == 3 then
raritySel = 2
end
end
if raritySel == 2 or raritySel == 3 then
item.crafted = true
end
item.rarity = controls.rarity.list[raritySel].rarity
if raritySel >= 3 then
item.title = controls.title.buf:match("%S") and controls.title.buf or "New Item"
end
if base.base.implicit then
local implicitIndex = 1
for line in base.base.implicit:gmatch("[^\n]+") do
local modList, extra = modLib.parseMod(line)
t_insert(item.implicitModLines, { line = line, extra = extra, modList = modList or { }, modTags = base.base.implicitModTypes and base.base.implicitModTypes[implicitIndex] or { } })
implicitIndex = implicitIndex + 1
end
end
item:NormaliseQuality()
item:BuildAndParseRaw()
return item
end
controls.rarityLabel = new("LabelControl", {"TOPRIGHT",nil,"TOPLEFT"}, {50, 20, 0, 16}, "Rarity:")
controls.rarity = new("DropDownControl", nil, {-80, 20, 100, 18}, rarityDropList)
controls.rarity.selIndex = self.lastCraftRaritySel or 3
controls.title = new("EditControl", nil, {70, 20, 190, 18}, "", "Name")
controls.title.shown = function()
return controls.rarity.selIndex >= 3
end
controls.typeLabel = new("LabelControl", {"TOPRIGHT",nil,"TOPLEFT"}, {50, 45, 0, 16}, "Type:")
controls.type = new("DropDownControl", {"TOPLEFT",nil,"TOPLEFT"}, {55, 45, 295, 18}, self.build.data.itemBaseTypeList, function(index, value)
controls.base.list = self.build.data.itemBaseLists[self.build.data.itemBaseTypeList[index]]
controls.base.selIndex = 1
end)
controls.type.selIndex = self.lastCraftTypeSel or 1
controls.baseLabel = new("LabelControl", {"TOPRIGHT",nil,"TOPLEFT"}, {50, 70, 0, 16}, "Base:")
controls.base = new("DropDownControl", {"TOPLEFT",nil,"TOPLEFT"}, {55, 70, 200, 18}, self.build.data.itemBaseLists[self.build.data.itemBaseTypeList[controls.type.selIndex]])
controls.base.selIndex = self.lastCraftBaseSel or 1
controls.base.tooltipFunc = function(tooltip, mode, index, value)
tooltip:Clear()
if mode ~= "OUT" then
self:AddItemTooltip(tooltip, makeItem(value), nil, true)
end
end
controls.save = new("ButtonControl", nil, {-45, 100, 80, 20}, "Create", function()
main:ClosePopup()
local item = makeItem(controls.base.list[controls.base.selIndex])
self:SetDisplayItem(item)
if not item.crafted and item.rarity ~= "NORMAL" then
self:EditDisplayItemText()
end
self.lastCraftRaritySel = controls.rarity.selIndex
self.lastCraftTypeSel = controls.type.selIndex
self.lastCraftBaseSel = controls.base.selIndex
end)
controls.cancel = new("ButtonControl", nil, {45, 100, 80, 20}, "Cancel", function()
main:ClosePopup()
end)
main:OpenPopup(370, 130, "Craft Item", controls)
end
-- Opens the item text editor popup
function ItemsTabClass:EditDisplayItemText(alsoAddItem)
local controls = { }
local function buildRaw()
local editBuf = controls.edit.buf
if editBuf:match("^Item Class: .*\nRarity: ") or editBuf:match("^Rarity: ") then
return editBuf
else
return "Rarity: "..controls.rarity.list[controls.rarity.selIndex].rarity.."\n"..controls.edit.buf
end
end
controls.rarity = new("DropDownControl", nil, {-190, 10, 100, 18}, rarityDropList)
controls.edit = new("EditControl", nil, {0, 40, 480, 420}, "", nil, "^%C\t\n", nil, nil, 14)
if self.displayItem then
controls.edit:SetText(self.displayItem:BuildRaw():gsub("Rarity: %w+\n",""))
controls.rarity:SelByValue(self.displayItem.rarity, "rarity")
else
controls.rarity.selIndex = 3
end
controls.edit.font = "FIXED"
controls.edit.pasteFilter = sanitiseText
controls.save = new("ButtonControl", nil, {-45, 470, 80, 20}, self.displayItem and "Save" or "Create", function()
local id = self.displayItem and self.displayItem.id
self:CreateDisplayItemFromRaw(buildRaw(), not self.displayItem)
self.displayItem.id = id
if alsoAddItem then
self:AddDisplayItem()
end
main:ClosePopup()
end, nil, true)
controls.save.enabled = function()
local item = new("Item", buildRaw())
return item.base ~= nil
end
controls.save.tooltipFunc = function(tooltip)
tooltip:Clear()
local item = new("Item", buildRaw())
if item.base then
self:AddItemTooltip(tooltip, item, nil, true)
else
tooltip:AddLine(14, "The item is invalid.")
tooltip:AddLine(14, "Check that the item's title and base name are in the correct format.")
tooltip:AddLine(14, "For Rare and Unique items, the first 2 lines must be the title and base name. E.g.:")
tooltip:AddLine(14, "Abberath's Horn")
tooltip:AddLine(14, "Goat's Horn")
tooltip:AddLine(14, "For Normal and Magic items, the base name must be somewhere in the first line. E.g.:")
tooltip:AddLine(14, "Scholar's Platinum Kris of Joy")
end
end
controls.cancel = new("ButtonControl", nil, {45, 470, 80, 20}, "Cancel", function()
main:ClosePopup()
end)
main:OpenPopup(500, 500, self.displayItem and "Edit Item Text" or "Create Custom Item from Text", controls, nil, "edit")
end
-- Opens the item enchanting popup
function ItemsTabClass:EnchantDisplayItem(enchantSlot)
self.enchantSlot = enchantSlot or 1
local controls = { }
local enchantments = self.displayItem.enchantments
local haveSkills = true
for _, source in ipairs(self.build.data.enchantmentSource) do
if self.displayItem.enchantments[source.name] then
haveSkills = false
break
end
end
local skillList = { }
local skillsUsed = { }
if haveSkills then
for _, socketGroup in ipairs(self.build.skillsTab.socketGroupList) do
for _, gemInstance in ipairs(socketGroup.gemList) do
if gemInstance.gemData then
for _, grantedEffect in ipairs(gemInstance.gemData.grantedEffectList) do
if not grantedEffect.support and enchantments[grantedEffect.name] then
skillsUsed[grantedEffect.name] = true
end
end
end
end
end
end
local function buildSkillList(onlyUsedSkills)
wipeTable(skillList)
for skillName in pairs(enchantments) do
if not onlyUsedSkills or not next(skillsUsed) or skillsUsed[skillName] then
t_insert(skillList, skillName)
end
end
table.sort(skillList)
end
local enchantmentSourceList = { }
local function buildEnchantmentSourceList()
wipeTable(enchantmentSourceList)
local list = haveSkills and enchantments[skillList[controls.skill and controls.skill.selIndex or 1]] or enchantments
for _, source in ipairs(self.build.data.enchantmentSource) do
if list[source.name] then
t_insert(enchantmentSourceList, source)
end
end
end
local enchantmentList = { }
local function buildEnchantmentList()
wipeTable(enchantmentList)
local list = haveSkills and enchantments[skillList[controls.skill and controls.skill.selIndex or 1]] or enchantments
for _, enchantment in ipairs(list[enchantmentSourceList[controls.enchantmentSource and controls.enchantmentSource.selIndex or 1].name]) do
t_insert(enchantmentList, enchantment)
end
end
if haveSkills then
buildSkillList(true)
end
buildEnchantmentSourceList()
buildEnchantmentList()
local function enchantItem(idx, remove)
local item = new("Item", self.displayItem:BuildRaw())
local index = idx or controls.enchantment.selIndex
item.id = self.displayItem.id
local list = haveSkills and enchantments[controls.skill.list[controls.skill.selIndex]] or enchantments
local line = list[controls.enchantmentSource.list[controls.enchantmentSource.selIndex].name][index]
local first, second = line:match("([^/]+)/([^/]+)")
if remove then
t_remove(item.enchantModLines, self.enchantSlot)
elseif first then
item.enchantModLines = { { crafted = true, line = first }, { crafted = true, line = second } }
else
if not item.canHaveTwoEnchants and #item.enchantModLines > 1 then
item.enchantModLines = { item.enchantModLines[1] }
end
if #item.enchantModLines >= self.enchantSlot then
t_remove(item.enchantModLines, self.enchantSlot)
end
t_insert(item.enchantModLines, self.enchantSlot, { crafted = true, line = line})
end
item:BuildAndParseRaw()
return item
end
if haveSkills then
controls.skillLabel = new("LabelControl", {"TOPRIGHT",nil,"TOPLEFT"}, {95, 20, 0, 16}, "^7Skill:")
controls.skill = new("DropDownControl", {"TOPLEFT",nil,"TOPLEFT"}, {100, 20, 180, 18}, skillList, function(index, value)
buildEnchantmentSourceList()
buildEnchantmentList()
controls.enchantment:SetSel(1)
end)
controls.allSkills = new("CheckBoxControl", {"TOPLEFT",nil,"TOPLEFT"}, {350, 20, 18}, "All skills:", function(state)
buildSkillList(not state)
controls.skill:SetSel(1)
buildEnchantmentList()
controls.enchantment:SetSel(1)
end)
controls.allSkills.tooltipText = "Show all skills, not just those used by this build."
if not next(skillsUsed) then
controls.allSkills.state = true
controls.allSkills.enabled = false
end
end
controls.enchantmentSourceLabel = new("LabelControl", {"TOPRIGHT",nil,"TOPLEFT"}, {95, 45, 0, 16}, "^7Source:")
controls.enchantmentSource = new("DropDownControl", {"TOPLEFT",nil,"TOPLEFT"}, {100, 45, 180, 18}, enchantmentSourceList, function(index, value)
buildEnchantmentList()
controls.enchantment:SetSel(m_min(controls.enchantment.selIndex, #enchantmentList))
end)
controls.enchantmentLabel = new("LabelControl", {"TOPRIGHT",nil,"TOPLEFT"}, {95, 70, 0, 16}, "^7Enchantment:")
controls.enchantment = new("DropDownControl", {"TOPLEFT",nil,"TOPLEFT"}, {100, 70, 440, 18}, enchantmentList)
controls.enchantment.tooltipFunc = function(tooltip, mode, index)
tooltip:Clear()
self:AddItemTooltip(tooltip, enchantItem(index), nil, true)
end
controls.save = new("ButtonControl", nil, {-88, 100, 80, 20}, "Enchant", function()
self:SetDisplayItem(enchantItem())
main:ClosePopup()
end)
controls.remove = new("ButtonControl", nil, {0, 100, 80, 20}, "Remove", function()
self:SetDisplayItem(enchantItem(nil, true))
main:ClosePopup()
end)
controls.close = new("ButtonControl", nil, {88, 100, 80, 20}, "Cancel", function()
main:ClosePopup()
end)
main:OpenPopup(550, 130, "Enchant Item", controls)
end
---Gets the name of the anointed node on an item
---@param item table @The item to get the anoint from
---@return string @The name of the anointed node, or nil if there is no anoint
function ItemsTabClass:getAnoint(item)
local result = { }
if item then
for _, modList in ipairs{item.enchantModLines, item.scourgeModLines, item.implicitModLines, item.explicitModLines, item.crucibleModLines} do
for _, mod in ipairs(modList) do
local line = mod.line
local anoint = line:find("Allocates ([a-zA-Z ]+)")
if anoint then
local nodeName = line:sub(anoint + string.len("Allocates "))
t_insert(result, nodeName)
end
end
end
end
return result
end
---Returns a copy of the currently displayed item, but anointed with a new node.
---Removes any existing enchantments before anointing. (Anoints are considered enchantments)
---@param node table @The passive tree node to anoint, or nil to just remove existing anoints.
---@return table @The new item
function ItemsTabClass:anointItem(node)
self.anointEnchantSlot = self.anointEnchantSlot or 1
local item = new("Item", self.displayItem:BuildRaw())
item.id = self.displayItem.id
if #item.enchantModLines >= self.anointEnchantSlot then
t_remove(item.enchantModLines, self.anointEnchantSlot)
end
if node then
t_insert(item.enchantModLines, self.anointEnchantSlot, { crafted = true, line = "Allocates " .. node.dn })
end
item:BuildAndParseRaw()
return item
end
---Appends tooltip information for anointing a new passive tree node onto the currently editing amulet
---@param tooltip table @The tooltip to append into
---@param node table @The passive tree node that will be anointed, or nil to remove the current anoint.
function ItemsTabClass:AppendAnointTooltip(tooltip, node, actionText)
if not self.displayItem then
return
end
if not actionText then
actionText = "Anointing"
end
local header
if node then
if self.build.spec.allocNodes[node.id] then
tooltip:AddLine(14, "^7"..actionText.." "..node.dn.." changes nothing because this node is already allocated on the tree.")
return
end
local curAnoints = self:getAnoint(self.displayItem)
if curAnoints and #curAnoints > 0 then
for _, curAnoint in ipairs(curAnoints) do
if curAnoint == node.dn then
tooltip:AddLine(14, "^7"..actionText.." "..node.dn.." changes nothing because this node is already anointed.")
return
end
end
end
header = "^7"..actionText.." "..node.dn.." will give you: "
else
header = "^7"..actionText.." nothing will give you: "
end
local calcFunc = self.build.calcsTab:GetMiscCalculator()
local outputBase = calcFunc({ repSlotName = "Amulet", repItem = self.displayItem })
local outputNew = calcFunc({ repSlotName = "Amulet", repItem = self:anointItem(node) })
local numChanges = self.build:AddStatComparesToTooltip(tooltip, outputBase, outputNew, header)
if node and numChanges == 0 then
tooltip:AddLine(14, "^7"..actionText.." "..node.dn.." changes nothing.")
end
end
---Appends tooltip with information about added notable passive node if it would be allocated.
---@param tooltip table @The tooltip to append into
---@param node table @The passive tree node that will be added
function ItemsTabClass:AppendAddedNotableTooltip(tooltip, node)
local calcFunc, calcBase = self.build.calcsTab:GetMiscCalculator()
local outputNew = calcFunc({ addNodes = { [node] = true } })
local numChanges = self.build:AddStatComparesToTooltip(tooltip, calcBase, outputNew, "^7Allocating "..node.dn.." will give you: ")
if numChanges == 0 then
tooltip:AddLine(14, "^7Allocating "..node.dn.." changes nothing.")
end
end
-- Opens the item anointing popup
function ItemsTabClass:AnointDisplayItem(enchantSlot)
self.anointEnchantSlot = enchantSlot or 1
local controls = { }
controls.notableDB = new("NotableDBControl", {"TOPLEFT",nil,"TOPLEFT"}, {10, 60, 360, 360}, self, self.build.spec.tree.nodes, "ANOINT")
local function saveLabel()
local node = controls.notableDB.selValue
if node then
return "Anoint " .. node.dn
end
local curAnoints = self:getAnoint(self.displayItem)
if curAnoints and #curAnoints >= self.anointEnchantSlot then
return "Remove "..curAnoints[self.anointEnchantSlot]
end
return "No Anoint"
end
local function saveLabelWidth()
local label = saveLabel()
return DrawStringWidth(16, "VAR", label) + 10
end
local function saveLabelX()
local width = saveLabelWidth()
return -(width + 90) / 2
end
controls.save = new("ButtonControl", {"BOTTOMLEFT", nil, "BOTTOM" }, {saveLabelX, -4, saveLabelWidth, 20}, saveLabel, function()
self:SetDisplayItem(self:anointItem(controls.notableDB.selValue))
main:ClosePopup()
end)
controls.save.tooltipFunc = function(tooltip)
tooltip:Clear()
self:AppendAnointTooltip(tooltip, controls.notableDB.selValue)
end
controls.close = new("ButtonControl", {"TOPLEFT", controls.save, "TOPRIGHT" }, {10, 0, 80, 20}, "Cancel", function()
main:ClosePopup()
end)
main:OpenPopup(380, 448, "Anoint Item", controls)
end
-- Opens the item corrupting popup
function ItemsTabClass:CorruptDisplayItem(modType)
local currentModType = modType or "Corrupted"
local controls = { }
local implicitList = { }
local sourceList = { "Corrupted", "Scourge" }
local function buildImplicitList(modType)
if implicitList[modType] then
return
end
implicitList[modType] = {}
for modId, mod in pairs(self.displayItem.affixes) do
if mod.type == modType and self.displayItem:GetModSpawnWeight(mod) > 0 then
t_insert(implicitList[modType], mod)
end
end
table.sort(implicitList[modType], function(a, b)
local an = a[1]:lower():gsub("%(.-%)","$"):gsub("[%+%-%%]",""):gsub("%d+","$")
local bn = b[1]:lower():gsub("%(.-%)","$"):gsub("[%+%-%%]",""):gsub("%d+","$")
if an ~= bn then
return an < bn
else
return a.level < b.level
end
end)
end
buildImplicitList(currentModType)
local function buildList(control, other, modType)
local selfMod = control.selIndex and control.selIndex > 1 and control.list[control.selIndex].mod
local otherMod = other and other.selIndex and other.selIndex > 1 and other.list[other.selIndex].mod
wipeTable(control.list)
t_insert(control.list, { label = "None" })
for _, mod in ipairs(implicitList[modType]) do
if not otherMod or mod.group ~= otherMod.group then
t_insert(control.list, { label = table.concat(mod, "/"), mod = mod })
end
end
control:SelByValue(selfMod, "mod")
end
local function corruptItem()
local item = new("Item", self.displayItem:BuildRaw())
item.id = self.displayItem.id
item.corrupted = true
local newImplicit = { }
for _, control in ipairs{controls.implicit, controls.implicit2, controls.implicit3, controls.implicit4} do
if control.selIndex > 1 then
local mod = control.list[control.selIndex].mod
for _, modLine in ipairs(mod) do
modLine = (currentModType == "ScourgeUpside" and "{scourge}" or "") .. modLine
if mod.modTags[1] then
t_insert(newImplicit, { line = "{tags:" .. table.concat(mod.modTags, ",") .. "}" .. modLine })
else
t_insert(newImplicit, { line = modLine })
end
end
end
end
if #newImplicit > 0 then
wipeTable(currentModType == "Corrupted" and item.implicitModLines or item.scourgeModLines)
for i, implicit in ipairs(newImplicit) do
t_insert(currentModType == "Corrupted" and item.implicitModLines or item.scourgeModLines, i, implicit)
end
end
item:BuildAndParseRaw()
return item
end
controls.sourceLabel = new("LabelControl", {"TOPRIGHT",nil,"TOPLEFT"}, {95, 20, 0, 16}, "^7Source:")
controls.source = new("DropDownControl", {"TOPLEFT",nil,"TOPLEFT"}, {100, 20, 150, 18}, sourceList, function(index, value)
if value == "Scourge" then
currentModType = "ScourgeUpside"
buildImplicitList("ScourgeUpside")
buildImplicitList("ScourgeDownside")
controls.implicit.shown = true
controls.implicitLabel.shown = true
controls.implicit2.shown = true
controls.implicit2Label.shown = true
controls.implicit3.shown = true
controls.implicit3Label.shown = true
controls.implicitCannotBeChangedLabel.shown = false
main.popups[1].height = 147
controls.close.y = 117
controls.save.y = 117
if self.displayItem.rarity == "UNIQUE" or self.displayItem.rarity == "RELIC" then
controls.implicit4Label.shown = true
controls.implicit4.shown = true
main.popups[1].height = 165
controls.close.y = 135
controls.save.y = 135
end
controls.implicit2.y = 85
buildList(controls.implicit3, controls.implicit4, "ScourgeDownside")
buildList(controls.implicit4, controls.implicit3, "ScourgeDownside")
else
currentModType = value
controls.implicit.shown = not self.displayItem.implicitsCannotBeChanged
controls.implicitLabel.shown = not self.displayItem.implicitsCannotBeChanged
controls.implicit2.shown = not self.displayItem.implicitsCannotBeChanged
controls.implicit2Label.shown = not self.displayItem.implicitsCannotBeChanged
controls.implicit3Label.shown = false
controls.implicit3.shown = false
controls.implicit4Label.shown = false
controls.implicit4.shown = false
controls.implicitCannotBeChangedLabel.shown = self.displayItem.implicitsCannotBeChanged
controls.implicit2.y = 65
main.popups[1].height = 129
controls.close.y = 99
controls.save.y = 99
end
buildList(controls.implicit, controls.implicit2, currentModType)
buildList(controls.implicit2, controls.implicit, currentModType)
controls.implicit:SetSel(1)
controls.implicit2:SetSel(1)
controls.implicit3:SetSel(1)
controls.implicit4:SetSel(1)
end)
controls.source.enabled = #sourceList > 1
controls.implicitLabel = new("LabelControl", {"TOPRIGHT",nil,"TOPLEFT"}, {75, 45, 0, 16}, "^7Implicit #1:")
controls.implicit = new("DropDownControl", {"TOPLEFT",nil,"TOPLEFT"}, {80, 45, 440, 18}, nil, function()
buildList(controls.implicit2, controls.implicit, currentModType)
end)
controls.implicit.tooltipFunc = function(tooltip, mode, index, value)
tooltip:Clear()
if mode ~= "OUT" and value and value.mod then
for _, line in ipairs(value.mod) do
tooltip:AddLine(16, "^7"..line)
end
self:AddModComparisonTooltip(tooltip, value.mod)
end
end
controls.implicit.shown = not self.displayItem.implicitsCannotBeChanged
controls.implicitLabel.shown = not self.displayItem.implicitsCannotBeChanged
controls.implicit2Label = new("LabelControl", {"TOPRIGHT",nil,"TOPLEFT"}, {75, 65, 0, 16}, "^7Implicit #2:")
controls.implicit2 = new("DropDownControl", {"TOPLEFT",nil,"TOPLEFT"}, {80, 65, 440, 18}, nil, function()
buildList(controls.implicit, controls.implicit2, currentModType)
end)
controls.implicit2.tooltipFunc = function(tooltip, mode, index, value)
tooltip:Clear()
if mode ~= "OUT" and value and value.mod then
for _, line in ipairs(value.mod) do
tooltip:AddLine(16, "^7"..line)
end
self:AddModComparisonTooltip(tooltip, value.mod)
end
end
controls.implicit2.shown = not self.displayItem.implicitsCannotBeChanged
controls.implicit2Label.shown = not self.displayItem.implicitsCannotBeChanged
controls.implicit3Label = new("LabelControl", {"TOPRIGHT",nil,"TOPLEFT"}, {75, 85, 0, 16}, "^7Implicit #3:")
controls.implicit3 = new("DropDownControl", {"TOPLEFT",nil,"TOPLEFT"}, {80, 65, 440, 18}, nil, function()
buildList(controls.implicit4, controls.implicit3, "ScourgeDownside")
end)
controls.implicit3.tooltipFunc = function(tooltip, mode, index, value)
tooltip:Clear()
if mode ~= "OUT" and value and value.mod then
for _, line in ipairs(value.mod) do
tooltip:AddLine(16, "^7"..line)
end
self:AddModComparisonTooltip(tooltip, value.mod)
end
end
controls.implicit3Label.shown = false
controls.implicit3.shown = false
controls.implicit4Label = new("LabelControl", {"TOPRIGHT",nil,"TOPLEFT"}, {75, 105, 0, 16}, "^7Implicit #4:")
controls.implicit4 = new("DropDownControl", {"TOPLEFT",nil,"TOPLEFT"}, {80, 105, 440, 18}, nil, function()
buildList(controls.implicit3, controls.implicit4, "ScourgeDownside")
end)
controls.implicit4.tooltipFunc = function(tooltip, mode, index, value)
tooltip:Clear()
if mode ~= "OUT" and value and value.mod then
for _, line in ipairs(value.mod) do
tooltip:AddLine(16, "^7"..line)
end
self:AddModComparisonTooltip(tooltip, value.mod)
end
end
controls.implicit4Label.shown = false
controls.implicit4.shown = false
controls.implicitCannotBeChangedLabel = new("LabelControl", {"TOPLEFT",nil,"TOPLEFT"}, {20, 45, 0, 20}, "^7This Items Implicits Cannot Be Changed")
controls.implicitCannotBeChangedLabel.shown = self.displayItem.implicitsCannotBeChanged
buildList(controls.implicit, controls.implicit2, currentModType)
buildList(controls.implicit2, controls.implicit, currentModType)
controls.save = new("ButtonControl", nil, {-45, 99, 80, 20}, modType, function()
self:SetDisplayItem(corruptItem())
main:ClosePopup()
end)
controls.save.tooltipFunc = function(tooltip)
tooltip:Clear()
self:AddItemTooltip(tooltip, corruptItem(), nil, true)
end
controls.close = new("ButtonControl", nil, {45, 99, 80, 20}, "Cancel", function()
main:ClosePopup()
end)
main:OpenPopup(540, 129, modType .. " Item", controls)
end
-- Opens the custom modifier popup
function ItemsTabClass:AddCustomModifierToDisplayItem()
local controls = { }
local sourceList = { }
local modList = { }
---Mutates modList to contain mods from the specified source
---@param sourceId string @The crafting source id to build the list of mods for
local function buildMods(sourceId)
wipeTable(modList)
if sourceId == "MASTER" then
local excludeGroups = { }
for _, modLine in ipairs({ self.displayItem.prefixes, self.displayItem.suffixes }) do
for i = 1, (modLine.limit or (self.displayItem.affixLimit / 2)) do
if modLine[i] and modLine[i].modId ~= "None" then
excludeGroups[self.displayItem.affixes[modLine[i].modId].group] = true
end
end
end
for i, craft in ipairs(self.build.data.masterMods) do
if craft.types[self.displayItem.type] and not excludeGroups[craft.group] then
t_insert(modList, {
label = table.concat(craft, "/") .. " ^8(" .. craft.type .. ")",
mod = craft,
type = "crafted",
affixType = craft.type,
defaultOrder = i,
})
end
end
table.sort(modList, function(a, b)
if a.affixType ~= b.affixType then
return a.affixType == "Prefix" and b.affixType == "Suffix"
else
return a.defaultOrder < b.defaultOrder
end
end)
elseif sourceId == "ESSENCE" then
for _, essence in pairs(self.build.data.essences) do
local modId = essence.mods[self.displayItem.type]
local mod = self.displayItem.affixes[modId]
t_insert(modList, {
label = essence.name .. " " .. "^8[" .. table.concat(mod, "/") .. "]" .. " (" .. mod.type .. ")",
mod = mod,
type = "custom",
essence = essence,
})
end
table.sort(modList, function(a, b)
if a.essence.type ~= b.essence.type then
return a.essence.type > b.essence.type
else
return a.essence.tier > b.essence.tier
end
end)
elseif sourceId == "PREFIX" or sourceId == "SUFFIX" then
for _, mod in pairs(self.displayItem.affixes) do
if sourceId:lower() == mod.type:lower() and self.displayItem:GetModSpawnWeight(mod) > 0 then
t_insert(modList, {
label = mod.affix .. " ^8[" .. table.concat(mod, "/") .. "]",
mod = mod,
type = "custom",
})
end
end
table.sort(modList, function(a, b)
local modA = a.mod
local modB = b.mod
for i = 1, m_max(#modA, #modB) do
if not modA[i] then
return true
elseif not modB[i] then
return false
elseif modA.statOrder[i] ~= modB.statOrder[i] then
return modA.statOrder[i] < modB.statOrder[i]
end
end
return modA.level > modB.level
end)
elseif sourceId == "VEILED" then
for i, mod in pairs(self.build.data.veiledMods) do
if self.displayItem:GetModSpawnWeight(mod) > 0 then
t_insert(modList, {
label = table.concat(mod, "/") .. " (" .. mod.type .. ")",
mod = mod,
affixType = mod.type,
type = "custom",
defaultOrder = i,
})
end
end
table.sort(modList, function(a, b)
if a.affixType ~= b.affixType then
return a.affixType == "Prefix" and b.affixType == "Suffix"
else
return a.defaultOrder < b.defaultOrder
end
end)
elseif sourceId == "DELVE" then
for i, mod in pairs(self.displayItem.affixes) do
if self.displayItem:CheckIfModIsDelve(mod) and self.displayItem:GetModSpawnWeight(mod) > 0 then
t_insert(modList, {
label = table.concat(mod, "/") .. " (" .. mod.type .. ")",
mod = mod,
affixType = mod.type,
type = "custom",
defaultOrder = i,
})
end
end
table.sort(modList, function(a, b)
if a.affixType ~= b.affixType then
return a.affixType == "Prefix" and b.affixType == "Suffix"
else
return a.defaultOrder < b.defaultOrder
end
end)
elseif sourceId == "NECROPOLIS" then
for i, mod in pairs(self.build.data.necropolisMods) do
if self.displayItem:GetNecropolisModSpawnWeight(mod) > 0 then
t_insert(modList, {
label = table.concat(mod, "/") .. " (" .. mod.type .. ")",
mod = mod,
affixType = mod.type,
type = "custom",
defaultOrder = i,
})
end
end
table.sort(modList, function(a, b)
if a.affixType ~= b.affixType then
return a.affixType == "Prefix" and b.affixType == "Suffix"
else
return a.defaultOrder < b.defaultOrder
end
end)
elseif sourceId == "BEASTCRAFT" then
for i, mod in pairs(self.build.data.beastCraft) do
t_insert(modList, {
label = table.concat(mod, "/") .. " (" .. mod.type .. ")",
mod = mod,
affixType = mod.type,
type = "custom",
defaultOrder = i,
})
end
table.sort(modList, function(a, b)
if a.affixType ~= b.affixType then
return a.affixType == "Prefix" and b.affixType == "Suffix"
else
return a.defaultOrder < b.defaultOrder
end
end)
end
end
if self.displayItem.type ~= "Tincture" then
if self.displayItem.type ~= "Jewel" then
t_insert(sourceList, { label = "Crafting Bench", sourceId = "MASTER" })
end
if self.displayItem.type ~= "Jewel" and self.displayItem.type ~= "Flask" then
t_insert(sourceList, { label = "Essence", sourceId = "ESSENCE" })
t_insert(sourceList, { label = "Veiled", sourceId = "VEILED"})
t_insert(sourceList, { label = "Beastcraft", sourceId = "BEASTCRAFT" })
end
if self.displayItem.type == "Helmet" or self.displayItem.type == "Body Armour" or self.displayItem.type == "Gloves" or self.displayItem.type == "Boots" then
t_insert(sourceList, { label = "Necropolis", sourceId = "NECROPOLIS"})
end
if not self.displayItem.clusterJewel and self.displayItem.type ~= "Flask" then
t_insert(sourceList, { label = "Delve", sourceId = "DELVE"})
end
if not self.displayItem.crafted then
t_insert(sourceList, { label = "Prefix", sourceId = "PREFIX" })
t_insert(sourceList, { label = "Suffix", sourceId = "SUFFIX" })
end
end
t_insert(sourceList, { label = "Custom", sourceId = "CUSTOM" })
buildMods(sourceList[1].sourceId)
local function addModifier()
local item = new("Item", self.displayItem:BuildRaw())
item.id = self.displayItem.id
local sourceId = sourceList[controls.source.selIndex].sourceId
if sourceId == "CUSTOM" then
if controls.custom.buf:match("%S") then
t_insert(item.explicitModLines, { line = controls.custom.buf, custom = true })
end
else
local listMod = modList[controls.modSelect.selIndex]
for _, line in ipairs(listMod.mod) do
t_insert(item.explicitModLines, { line = line, modTags = listMod.mod.modTags, [listMod.type] = true })
end
end
item:BuildAndParseRaw()
return item
end
controls.sourceLabel = new("LabelControl", {"TOPRIGHT",nil,"TOPLEFT"}, {95, 20, 0, 16}, "^7Source:")
controls.source = new("DropDownControl", {"TOPLEFT",nil,"TOPLEFT"}, {100, 20, 150, 18}, sourceList, function(index, value)
buildMods(value.sourceId)
controls.modSelect:SetSel(1)
end)
controls.source.enabled = #sourceList > 1
controls.modSelectLabel = new("LabelControl", {"TOPRIGHT",nil,"TOPLEFT"}, {95, 45, 0, 16}, "^7Modifier:")
controls.modSelect = new("DropDownControl", {"TOPLEFT",nil,"TOPLEFT"}, {100, 45, 600, 18}, modList)
controls.modSelect.shown = function()
return sourceList[controls.source.selIndex].sourceId ~= "CUSTOM"
end
controls.modSelect.tooltipFunc = function(tooltip, mode, index, value)
tooltip:Clear()
if mode ~= "OUT" and value then
for _, line in ipairs(value.mod) do
tooltip:AddLine(16, "^7"..line)
end
self:AddModComparisonTooltip(tooltip, value.mod)
end
end
controls.custom = new("EditControl", {"TOPLEFT",nil,"TOPLEFT"}, {100, 45, 440, 18})
controls.custom.shown = function()
return sourceList[controls.source.selIndex].sourceId == "CUSTOM"
end
controls.save = new("ButtonControl", nil, {-45, 75, 80, 20}, "Add", function()
self:SetDisplayItem(addModifier())
main:ClosePopup()
end)
controls.save.tooltipFunc = function(tooltip)
tooltip:Clear()
self:AddItemTooltip(tooltip, addModifier())
end
controls.close = new("ButtonControl", nil, {45, 75, 80, 20}, "Cancel", function()
main:ClosePopup()
end)
main:OpenPopup(710, 105, "Add Modifier to Item", controls, "save", sourceList[controls.source.selIndex].sourceId == "CUSTOM" and "custom")
end
-- Opens the crucible modifier popup
function ItemsTabClass:AddCrucibleModifierToDisplayItem()
local controls = { }
local modList = {[1] = {"None"}, [2] = {"None"}, [3] = {"None"}, [4] = {"None"}, [5] = {"None"}}
local itemModMap, nodeSelections = { }, { }
local function getLabelFromMod(mod)
local label = copyTable(mod)
for index, line in ipairs(mod) do
label[index] = checkLineForAllocates(line, self.build.spec.nodes)
end
return table.concat(label, "/")
end
local function itemCanHaveMod(mod)
local keyMap, includeTags = { }, { }
for index, key in ipairs(mod.weightKey) do
keyMap[key] = index
end
-- check for uniques with off-tag mods
if data.casterTagCrucibleUniques[self.displayItem.title] then
includeTags["caster_unique_weapon"] = true
end
if data.minionTagCrucibleUniques[self.displayItem.title] then
includeTags["minion_unique_weapon"] = true
end
if self.displayItem.canHaveOnlySupportSkillsCrucibleTree then
return keyMap["crucible_unique_staff"] and mod.weightVal[keyMap["crucible_unique_staff"]] ~= 0
elseif self.displayItem.canHaveShieldCrucibleTree then
return self.displayItem:GetModSpawnWeight(mod, { ["crucible_unique_helmet"] = true, ["shield"] = true }) > 0
elseif self.displayItem.canHaveTwoHandedSwordCrucibleTree then
return self.displayItem:GetModSpawnWeight(mod, { ["two_hand_weapon"] = true }, { ["one_hand_weapon"] = true }) > 0
end
return self.displayItem:GetModSpawnWeight(mod, includeTags) > 0
end
local function buildCrucibleMods()
for i, mod in pairs(self.build.data.crucible) do
if itemCanHaveMod(mod) then
-- item mod must match the whole mod, whether that's one line or two
if itemModMap[checkLineForAllocates(mod[1], self.build.spec.nodes)] and ((mod[2] and itemModMap[checkLineForAllocates(mod[2], self.build.spec.nodes)]) or not mod[2]) then
-- for multi nodes, if the first location is taken, use second
-- works for multi vs single node, ambiguous for multi vs multi (3,4 vs 3,4) but both mods load
if nodeSelections[mod.nodeLocation[1]] and mod.nodeLocation[2] then
nodeSelections[mod.nodeLocation[2]] = i
-- nodeSelections[nodeId] = defaultOrder, used later to match with sorted modList to get selIndex
else
nodeSelections[mod.nodeLocation[1]] = i
end
end
for _, location in ipairs(mod.nodeLocation) do
t_insert(modList[location], {
label = getLabelFromMod(mod) .. " - Tier: " .. mod.tier,
mod = mod,
affixType = mod.type,
type = "crucible",
defaultOrder = i,
})
end
end
end
for _, tierList in ipairs(modList) do
table.sort(tierList, function(a, b)
if b ~= "None" then
if a.affixType ~= b.affixType then
return a.affixType == "Spawn" and b.affixType == "MergeOnly"
else
return a.defaultOrder < b.defaultOrder
end
end
end)
end
end
local function addModifier()
local item = new("Item", self.displayItem:BuildRaw())
item.id = self.displayItem.id
item.crucibleModLines = { }
local listMod = {
modList[1][controls.modSelectNode1.selIndex],
modList[2][controls.modSelectNode2.selIndex],
modList[3][controls.modSelectNode3.selIndex],
modList[4][controls.modSelectNode4.selIndex],
modList[5][controls.modSelectNode5.selIndex],
}
for _, nodeMod in ipairs(listMod) do
if nodeMod ~= "None" then
for index, line in ipairs(nodeMod.mod) do
t_insert(item.crucibleModLines, { line = checkLineForAllocates(line, self.build.spec.nodes), modTags = nodeMod.mod.modTags, [nodeMod.type] = true })
end
end
end
item:BuildAndParseRaw()
return item
end
-- set up name map to know what modLines the item has as we build the mods out
for _, mod in ipairs(self.displayItem.crucibleModLines) do
itemModMap[mod.line] = true
end
buildCrucibleMods()
local y = 45
for i = 1,5 do
controls["modSelectNode"..i.."Label"] = new("LabelControl", {"TOPRIGHT",nil,"TOPLEFT"}, {95, y, 0, 16}, "^7Node "..i..":")
controls["modSelectNode"..i] = new("DropDownControl", {"TOPLEFT",nil,"TOPLEFT"}, {100, y, 555, 18}, modList[i])
controls["modSelectNode"..i].tooltipFunc = function(tooltip, mode, index, value)
tooltip:Clear()
if mode ~= "OUT" and value and value ~= "None" then
for _, line in ipairs(value.mod) do
tooltip:AddLine(16, "^7"..checkLineForAllocates(line, self.build.spec.nodes))
end
self:AddModComparisonTooltip(tooltip, value.mod)
end
end
y = y + 22
end
-- populate dropdowns with item mods
for nodeId, defaultOrder in pairs(nodeSelections) do
for index, mod in pairs(modList[nodeId]) do
if defaultOrder == mod.defaultOrder then
controls["modSelectNode"..nodeId].selIndex = index
end
end
end
controls.save = new("ButtonControl", nil, {-45, 157, 80, 20}, "Add", function()
self:SetDisplayItem(addModifier())
main:ClosePopup()
end)
controls.save.tooltipFunc = function(tooltip)
tooltip:Clear()
self:AddItemTooltip(tooltip, addModifier())
end
controls.close = new("ButtonControl", nil, {45, 157, 80, 20}, "Cancel", function()
main:ClosePopup()
end)
main:OpenPopup(710, 185, "Add Crucible Modifier to Item", controls, "save")
end
-- Opens the custom Implicit popup
function ItemsTabClass:AddImplicitToDisplayItem()
local controls = { }
local sourceList = { }
local modList = { }
local modGroups = {}
---Mutates modList to contain mods from the specified source
---@param sourceId string @The crafting source id to build the list of mods for
local function buildMods(sourceId)
wipeTable(modList)
wipeTable(modGroups)
local groupIndexes = {}
if sourceId == "EXARCH" or sourceId == "EATER" then
for i, mod in pairs(self.displayItem.affixes) do
if self.displayItem:GetModSpawnWeight(mod) > 0 and sourceId:lower() == mod.type:lower() then
local modLabel = table.concat(mod, "/")
local group = mod.group:gsub("PinnaclePresence", ""):gsub("UniquePresence", "")
if not groupIndexes[group] then
t_insert(modList, {})
t_insert(modGroups, {
label = modLabel,
mod = mod,
modListIndex = #modList,
defaultOrder = i,
})
groupIndexes[group] = #modGroups
end
t_insert(modList[groupIndexes[group]], {
label = modLabel,
mod = mod,
affixType = mod.type,
type = sourceId:lower(),
defaultOrder = i,
})
end
end
table.sort(modGroups, function(a, b)
local modA = a.mod
local modB = b.mod
for i = 1, m_max(#modA, #modB) do
if not modA[i] then
return true
elseif not modB[i] then
return false
elseif modA.statOrder[i] ~= modB.statOrder[i] then
return modA.statOrder[i] < modB.statOrder[i]
end
end
return modA.level > modB.level
end)
for i, _ in pairs(modList) do
table.sort(modList[i], function(a, b)
local modA = a.mod
local modB = b.mod
if modA.group ~= modB.group then
if modA.group:match("PinnaclePresence") then
return false
elseif modB.group:match("PinnaclePresence") then
return true
elseif modA.group:match("UniquePresence") then
return false
else
return true
end
end
for j = 1, m_max(#modA, #modB) do
if not modA[j] then
return true
elseif not modB[j] then
return false
elseif modA.statOrder[j] ~= modB.statOrder[j] then
return modA.statOrder[j] < modB.statOrder[j]
else
local modAVal = tonumber(a.defaultOrder:match("%d+$"))
local modBVal = tonumber(b.defaultOrder:match("%d+$"))
return modAVal < modBVal
end
end
return modA.level > modB.level
end)
end
for i, _ in pairs(modGroups) do
modGroups[i].label = modList[modGroups[i].modListIndex][1].label:gsub("%([%d%.]+%-[%d%.]+%)", "#"):gsub("[%d%.]+", "#")
end
elseif sourceId == "SYNTHESIS" then
for i, mod in pairs(self.displayItem.affixes) do
if sourceId:lower() == mod.type:lower() then -- weights are missing and so are 0, how do I determine what goes on what item?, also arn't these supposed to work on jewels?
t_insert(modList, {
label = table.concat(mod, "/"),
mod = mod,
affixType = mod.type,
type = "synthesis",
defaultOrder = i,
})
end
end
table.sort(modList, function(a, b)
return a.defaultOrder < b.defaultOrder
end)
elseif sourceId == "DelveImplicit" then
for i, mod in pairs(self.displayItem.affixes) do
if self.displayItem:GetModSpawnWeight(mod) > 0 and sourceId:lower() == mod.type:lower() then
local modLabel = table.concat(mod, "/")
if not groupIndexes[mod.group] then
t_insert(modList, {})
t_insert(modGroups, {
label = modLabel,
mod = mod,
modListIndex = #modList,
defaultOrder = i,
})
groupIndexes[mod.group] = #modGroups
--elseif mod[1].len() < modGroups[groupIndexes[mod.group] ].mod[1].len() then
-- modGroups[groupIndexes[mod.group]].label = modLabel
-- modGroups[groupIndexes[mod.group]].mod = mod
end
t_insert(modList[groupIndexes[mod.group]], {
label = modLabel,
mod = mod,
affixType = mod.type,
type = "custom",
defaultOrder = i,
})
end
end
for i, _ in pairs(modList) do
table.sort(modList[i], function(a, b)
return a.defaultOrder < b.defaultOrder
end)
end
end
end
if (self.displayItem.rarity ~= "UNIQUE" and self.displayItem.rarity ~= "RELIC") and (self.displayItem.type == "Helmet" or self.displayItem.type == "Body Armour" or self.displayItem.type == "Gloves" or self.displayItem.type == "Boots") then
if self.displayItem.cleansing then
t_insert(sourceList, { label = "Searing Exarch", sourceId = "EXARCH" })
end
if self.displayItem.tangle then
t_insert(sourceList, { label = "Eater of Worlds", sourceId = "EATER" })
end
end
if self.displayItem.type ~= "Flask" and self.displayItem.type ~= "Jewel" then
--t_insert(sourceList, { label = "Synth", sourceId = "SYNTHESIS" }) -- synth removed until we get proper support for where the mods go
t_insert(sourceList, { label = "Delve", sourceId = "DelveImplicit" })
end
t_insert(sourceList, { label = "Custom", sourceId = "CUSTOM" })
buildMods(sourceList[1].sourceId)
local function addModifier()
local item = new("Item", self.displayItem:BuildRaw())
item.id = self.displayItem.id
local sourceId = sourceList[controls.source.selIndex].sourceId
if sourceId == "CUSTOM" then
if controls.custom.buf:match("%S") then
t_insert(item.implicitModLines, { line = controls.custom.buf, custom = true })
end
elseif sourceId == "SYNTHESIS" then
local listMod = modList[controls.modSelect.selIndex]
for _, line in ipairs(listMod.mod) do
t_insert(item.implicitModLines, { line = line, modTags = listMod.mod.modTags, [listMod.type] = true })
end
elseif sourceId == "EXARCH" or sourceId == "EATER" then
local listMod = modList[modGroups[controls.modGroupSelect.selIndex].modListIndex][controls.modSelect.selIndex]
local index
for i, implicitMod in ipairs(item.implicitModLines) do
if implicitMod[listMod.type] then
index = i
break
end
end
if index then
for i, line in ipairs(listMod.mod) do
item.implicitModLines[index + i - 1] = { line = line, modTags = listMod.mod.modTags, [listMod.type] = true }
end
else
for _, line in ipairs(listMod.mod) do
t_insert(item.implicitModLines, { line = line, modTags = listMod.mod.modTags, [listMod.type] = true })
end
end
else
local listMod = modList[modGroups[controls.modGroupSelect.selIndex].modListIndex][controls.modSelect.selIndex]
for _, line in ipairs(listMod.mod) do
t_insert(item.implicitModLines, { line = line, modTags = listMod.mod.modTags, [listMod.type] = true })
end
end
item:BuildAndParseRaw()
return item
end
controls.sourceLabel = new("LabelControl", {"TOPRIGHT",nil,"TOPLEFT"}, {95, 20, 0, 16}, "^7Source:")
controls.source = new("DropDownControl", {"TOPLEFT",nil,"TOPLEFT"}, {100, 20, 150, 18}, sourceList, function(index, value)
if value.sourceId ~= "CUSTOM" then
controls.modSelectLabel.y = 70
buildMods(value.sourceId)
controls.modGroupSelect:SetSel(1)
controls.modSelect.list = modList[modGroups[1].modListIndex]
controls.modSelect:SetSel(1)
else
controls.modSelectLabel.y = 45
end
end)
controls.source.enabled = #sourceList > 1
controls.modGroupSelectLabel = new("LabelControl", {"TOPRIGHT",nil,"TOPLEFT"}, {95, 45, 0, 16}, "^7Type:")
controls.modGroupSelect = new("DropDownControl", {"TOPLEFT",nil,"TOPLEFT"}, {100, 45, 600, 18}, modGroups, function(index, value)
controls.modSelect.list = modList[value.modListIndex]
controls.modSelect:SetSel(1)
end)
controls.modGroupSelectLabel.shown = function()
if sourceList[controls.source.selIndex].sourceId == "CUSTOM" then
controls.modSelectLabel.y = 45
end
return sourceList[controls.source.selIndex].sourceId ~= "CUSTOM"
end
controls.modGroupSelect.shown = function()
return sourceList[controls.source.selIndex].sourceId ~= "CUSTOM"
end
controls.modGroupSelect.tooltipFunc = function(tooltip, mode, index, value)
tooltip:Clear()
if mode ~= "OUT" and value then
for _, line in ipairs(value.mod) do
tooltip:AddLine(16, "^7"..line)
end
self:AddModComparisonTooltip(tooltip, value.mod)
end
end
controls.modSelectLabel = new("LabelControl", {"TOPRIGHT",nil,"TOPLEFT"}, {95, 70, 0, 16}, "^7Modifier:")
controls.modSelect = new("DropDownControl", {"TOPLEFT",nil,"TOPLEFT"}, {100, 70, 600, 18}, sourceList[controls.source.selIndex].sourceId ~= "CUSTOM" and modList[modGroups[1].modListIndex] or { })
controls.modSelect.shown = function()
return sourceList[controls.source.selIndex].sourceId ~= "CUSTOM"
end
controls.modSelect.tooltipFunc = function(tooltip, mode, index, value)
tooltip:Clear()
if mode ~= "OUT" and value then
for _, line in ipairs(value.mod) do
tooltip:AddLine(16, "^7"..line)
end
self:AddModComparisonTooltip(tooltip, value.mod)
end
end
controls.custom = new("EditControl", {"TOPLEFT",nil,"TOPLEFT"}, {100, 45, 440, 18})
controls.custom.shown = function()
return sourceList[controls.source.selIndex].sourceId == "CUSTOM"
end
controls.save = new("ButtonControl", nil, {-45, 100, 80, 20}, "Add", function()
self:SetDisplayItem(addModifier())
main:ClosePopup()
end)
controls.save.tooltipFunc = function(tooltip)
tooltip:Clear()
self:AddItemTooltip(tooltip, addModifier())
end
controls.close = new("ButtonControl", nil, {45, 100, 80, 20}, "Cancel", function()
main:ClosePopup()
end)
main:OpenPopup(710, 130, "Add Implicit to Item", controls, "save", sourceList[controls.source.selIndex].sourceId == "CUSTOM" and "custom")
end
function ItemsTabClass:AddItemSetTooltip(tooltip, itemSet)
for _, slot in ipairs(self.orderedSlots) do
if not slot.nodeId then
local item = self.items[itemSet[slot.slotName].selItemId]
if item then
tooltip:AddLine(16, s_format("^7%s: %s%s", slot.label, colorCodes[item.rarity], item.name))
end
end
end
end
function ItemsTabClass:SetTooltipHeaderInfluence(tooltip, item)
tooltip.influenceHeader1 = nil
tooltip.influenceHeader2 = nil
local function addInfluence(name)
if not tooltip.influenceHeader1 then
tooltip.influenceHeader1 = name
elseif not tooltip.influenceHeader2 then
tooltip.influenceHeader2 = name
end
end
if item.title and item.title:find("Replica") then
addInfluence("Experimented")
-- Eater and Exarch combo takes priority over fractured icon.
elseif item.cleansing and item.tangle then
addInfluence("Exarch")
addInfluence("Eater")
else
-- Dual influence with fracture will show fractured icon and highest priority influence.
if item.fractured then
addInfluence("Fractured")
end
if item.veiled then
addInfluence("Veiled")
end
if item.cleansing then
addInfluence("Exarch")
end
if item.tangle then
addInfluence("Eater")
end
if item.shaper then
addInfluence("Shaper")
end
if item.elder then
addInfluence("Elder")
end
if item.crusader then
addInfluence("Crusader")
end
if item.eyrie then
addInfluence("Redeemer")
end
if item.basilisk then
addInfluence("Hunter")
end
if item.adjudicator then
addInfluence("Warlord")
end
if item.synthesised and not tooltip.influenceHeader1 then
addInfluence("Synthesis")
end
end
if tooltip.influenceHeader1 and not tooltip.influenceHeader2 then
tooltip.influenceHeader2 = tooltip.influenceHeader1
end
end
function ItemsTabClass:FormatItemSource(text)
return text:gsub("unique{([^}]+)}",colorCodes.UNIQUE.."%1"..colorCodes.SOURCE)
:gsub("normal{([^}]+)}",colorCodes.NORMAL.."%1"..colorCodes.SOURCE)
:gsub("currency{([^}]+)}",colorCodes.CURRENCY.."%1"..colorCodes.SOURCE)
:gsub("prophecy{([^}]+)}",colorCodes.PROPHECY.."%1"..colorCodes.SOURCE)
end
function ItemsTabClass:AddItemTooltip(tooltip, item, slot, dbMode)
local fontSizeSmall = main.showFlavourText and 16 or 14
local fontSizeBig = main.showFlavourText and 18 or 16
local fontSizeTitle = main.showFlavourText and 22 or 20
local rarityCode = colorCodes[item.rarity]
tooltip.maxWidth = 600 -- Should instead get the longest mod and set the width to that. Some flavour text is way too long so we need a cap of sorts.
tooltip.tooltipHeader = item.rarity
tooltip.foilType = item.foilType
tooltip.center = true
tooltip.color = rarityCode
self:SetTooltipHeaderInfluence(tooltip, item)
-- Item name
if item.title then
tooltip:AddLine(fontSizeTitle, rarityCode..item.title, "FONTIN SC")
tooltip:AddLine(fontSizeTitle, rarityCode..item.baseName:gsub(" %(.+%)",""),"FONTIN SC")
else
tooltip:AddLine(fontSizeTitle, rarityCode..item.namePrefix..item.baseName:gsub(" %(.+%)","")..item.nameSuffix, "FONTIN SC")
end
for _, curInfluenceInfo in ipairs(influenceInfo) do
if item[curInfluenceInfo.key] and not main.showFlavourText then
tooltip:AddLine(fontSizeBig, curInfluenceInfo.color..curInfluenceInfo.display.." Item", "FONTIN SC")
end
end
if item.fractured and not main.showFlavourText then
tooltip:AddLine(fontSizeBig, colorCodes.FRACTURED.."Fractured Item")
end
if item.synthesised and not main.showFlavourText then
tooltip:AddLine(fontSizeBig, colorCodes.CRAFTED.."Synthesised Item")
end
tooltip:AddSeparator(10)
-- Special fields for database items
if dbMode then
if item.variantList then
if #item.variantList == 1 then
tooltip:AddLine(fontSizeBig, "^xFFFF30Variant: "..item.variantList[1], "FONTIN SC")
else
tooltip:AddLine(fontSizeBig, "^xFFFF30Variant: "..item.variantList[item.variant].." ("..#item.variantList.." variants)", "FONTIN SC")
end
end
if item.league then
tooltip:AddLine(fontSizeBig, "^xFF5555Exclusive to: "..item.league, "FONTIN SC")
end
if item.unreleased then
tooltip:AddLine(fontSizeBig, colorCodes.NEGATIVE.."Not yet available", "FONTIN SC")
end
if item.source then
tooltip:AddLine(fontSizeBig, colorCodes.SOURCE.."Source: "..self:FormatItemSource(item.source), "FONTIN SC")
end
if item.upgradePaths then
for _, path in ipairs(item.upgradePaths) do
tooltip:AddLine(fontSizeBig, colorCodes.SOURCE..self:FormatItemSource(path), "FONTIN SC")
end
end
tooltip:AddSeparator(10)
end
local base = item.base
local slotNum = slot and slot.slotNum or (IsKeyDown("SHIFT") and 2 or 1)
local modList = item.modList or item.slotModList[slotNum]
if base.weapon then
-- Weapon-specific info
local weaponData = item.weaponData[slotNum]
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7F%s", self.build.data.weaponTypeInfo[base.type].label or base.type), "FONTIN SC")
if item.quality > 0 then
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7FQuality: "..colorCodes.MAGIC.."+%d%%", item.quality), "FONTIN SC")
end
local totalDamageTypes = 0
if weaponData.PhysicalDPS then
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7FPhysical Damage: "..colorCodes.MAGIC.."%d-%d (%.1f DPS)", weaponData.PhysicalMin, weaponData.PhysicalMax, weaponData.PhysicalDPS), "FONTIN SC")
totalDamageTypes = totalDamageTypes + 1
end
if weaponData.ElementalDPS then
local elemLine
for _, var in ipairs({"Fire","Cold","Lightning"}) do
if weaponData[var.."DPS"] then
elemLine = elemLine and elemLine.."^x7F7F7F, " or "^x7F7F7FElemental Damage: "
elemLine = elemLine..s_format("%s%d-%d", colorCodes[var:upper()], weaponData[var.."Min"], weaponData[var.."Max"])
end
end
tooltip:AddLine(fontSizeBig, elemLine, "FONTIN SC")
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7FElemental DPS: "..colorCodes.MAGIC.."%.1f", weaponData.ElementalDPS), "FONTIN SC")
totalDamageTypes = totalDamageTypes + 1
end
if weaponData.ChaosDPS then
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7FChaos Damage: "..colorCodes.CHAOS.."%d-%d "..colorCodes.MAGIC.."(%.1f DPS)", weaponData.ChaosMin, weaponData.ChaosMax, weaponData.ChaosDPS), "FONTIN SC")
totalDamageTypes = totalDamageTypes + 1
end
if totalDamageTypes > 1 then
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7FTotal DPS: "..colorCodes.MAGIC.."%.1f", weaponData.TotalDPS), "FONTIN SC")
end
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7FCritical Strike Chance: %s%.2f%%", main:StatColor(weaponData.CritChance, base.weapon.CritChanceBase), weaponData.CritChance), "FONTIN SC")
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7FAttacks per Second: %s%.2f", main:StatColor(weaponData.AttackRate, base.weapon.AttackRateBase), weaponData.AttackRate), "FONTIN SC")
if weaponData.range < 120 then
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7FWeapon Range: %s%.1f ^x7F7F7Fmetres", main:StatColor(weaponData.range, base.weapon.Range), weaponData.range / 10), "FONTIN SC")
end
elseif base.armour then
-- Armour-specific info
local armourData = item.armourData
if item.quality > 0 then
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7FQuality: "..colorCodes.MAGIC.."+%d%%", item.quality), "FONTIN SC")
end
if base.armour.BlockChance and armourData.BlockChance > 0 then
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7FChance to Block: %s%d%%", main:StatColor(armourData.BlockChance, base.armour.BlockChance), armourData.BlockChance), "FONTIN SC")
end
if armourData.Armour > 0 then
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7FArmour: %s%d", main:StatColor(armourData.Armour, base.armour.ArmourBase), armourData.Armour), "FONTIN SC")
end
if armourData.Evasion > 0 then
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7FEvasion Rating: %s%d", main:StatColor(armourData.Evasion, base.armour.EvasionBase), armourData.Evasion), "FONTIN SC")
end
if armourData.EnergyShield > 0 then
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7FEnergy Shield: %s%d", main:StatColor(armourData.EnergyShield, base.armour.EnergyShieldBase), armourData.EnergyShield), "FONTIN SC")
end
if armourData.Ward > 0 then
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7FWard: %s%d", main:StatColor(armourData.Ward, base.armour.WardBase), armourData.Ward), "FONTIN SC")
end
elseif base.flask then
-- Flask-specific info
local flaskData = item.flaskData
if item.quality > 0 then
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7FQuality: "..colorCodes.MAGIC.."+%d%%", item.quality), "FONTIN SC")
end
if flaskData.lifeTotal then
if flaskData.lifeGradual ~= 0 then
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7FRecovers %s%d ^x7F7F7FLife over %s%.1f0 ^x7F7F7FSeconds",
main:StatColor(flaskData.lifeTotal, base.flask.life), flaskData.lifeGradual,
main:StatColor(flaskData.duration, base.flask.duration), flaskData.duration
), "FONTIN SC")
end
if flaskData.lifeInstant ~= 0 then
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7FRecovers %s%d ^x7F7F7FLife instantly", main:StatColor(flaskData.lifeTotal, base.flask.life), flaskData.lifeInstant), "FONTIN SC")
end
end
if flaskData.manaTotal then
if flaskData.manaGradual ~= 0 then
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7FRecovers %s%d ^x7F7F7FMana over %s%.1f0 ^x7F7F7FSeconds",
main:StatColor(flaskData.manaTotal, base.flask.mana), flaskData.manaGradual,
main:StatColor(flaskData.duration, base.flask.duration), flaskData.duration
), "FONTIN SC")
end
if flaskData.manaInstant ~= 0 then
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7FRecovers %s%d ^x7F7F7FMana instantly", main:StatColor(flaskData.manaTotal, base.flask.mana), flaskData.manaInstant), "FONTIN SC")
end
end
if not flaskData.lifeTotal and not flaskData.manaTotal then
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7FLasts %s%.2f ^x7F7F7FSeconds", main:StatColor(flaskData.duration, base.flask.duration), flaskData.duration), "FONTIN SC")
end
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7FConsumes %s%d ^x7F7F7Fof %s%d ^x7F7F7FCharges on use",
main:StatColor(flaskData.chargesUsed, base.flask.chargesUsed), flaskData.chargesUsed,
main:StatColor(flaskData.chargesMax, base.flask.chargesMax), flaskData.chargesMax
), "FONTIN SC")
for _, modLine in pairs(item.buffModLines) do
if modLine.extra then
local line = colorCodes.UNSUPPORTED..modLine.line
line = main.notSupportedModTooltips and (line .. main.notSupportedTooltipText) or line
tooltip:AddLine(fontSizeBig, line, "FONTIN SC")
else
tooltip:AddLine(fontSizeBig, colorCodes.MAGIC..modLine.line, "FONTIN SC")
end
end
elseif base.tincture then
-- Tincture-specific info
local tinctureData = item.tinctureData
if item.quality and item.quality > 0 then
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7FQuality: "..colorCodes.MAGIC.."+%d%%", item.quality), "FONTIN SC")
end
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7FInflicts Mana Burn every %s%.2f ^x7F7F7FSeconds", main:StatColor(tinctureData.manaBurn, base.tincture.manaBurn), tinctureData.manaBurn), "FONTIN SC")
tooltip:AddLine(fontSizeBig, s_format("^x7F7F7F%s%.2f ^x7F7F7FSecond Cooldown When Deactivated", main:StatColor(tinctureData.cooldown, base.tincture.cooldown), tinctureData.cooldown), "FONTIN SC")
for _, modLine in pairs(item.buffModLines) do
if modLine.extra then
local line = colorCodes.UNSUPPORTED..modLine.line
line = main.notSupportedModTooltips and (line .. main.notSupportedTooltipText) or line
tooltip:AddLine(fontSizeBig, line, "FONTIN SC")
else
tooltip:AddLine(fontSizeBig, colorCodes.MAGIC..modLine.line, "FONTIN SC")
end
end
elseif item.type == "Jewel" then
-- Jewel-specific info
if item.limit then
tooltip:AddLine(fontSizeBig, "^x7F7F7FLimited to: ^7"..item.limit, "FONTIN SC")
end
if item.classRestriction then
tooltip:AddLine(fontSizeBig, "^x7F7F7FRequires Class "..(self.build.spec.curClassName == item.classRestriction and colorCodes.POSITIVE or colorCodes.NEGATIVE)..item.classRestriction, "FONTIN SC")
end
if item.jewelRadiusLabel then
tooltip:AddLine(fontSizeBig, "^x7F7F7FRadius: ^7"..item.jewelRadiusLabel, "FONTIN SC")
end
if item.jewelRadiusData and slot and item.jewelRadiusData[slot.nodeId] then
local radiusData = item.jewelRadiusData[slot.nodeId]
local line
local codes = { colorCodes.STRENGTH, colorCodes.DEXTERITY, colorCodes.INTELLIGENCE }
for i, stat in ipairs({"Str","Dex","Int"}) do
if radiusData[stat] and radiusData[stat] ~= 0 then
line = (line and line .. ", " or "") .. s_format("%s%d %s^7", codes[i], radiusData[stat], stat)
end
end
if line then
tooltip:AddLine(fontSizeBig, "^x7F7F7FAttributes in Radius: "..line, "FONTIN SC")
end
end
end
if item.catalyst and item.catalyst > 0 and item.catalyst <= #catalystQualityFormat and item.catalystQuality and item.catalystQuality > 0 then
tooltip:AddLine(fontSizeBig, s_format(catalystQualityFormat[item.catalyst], item.catalystQuality), "FONTIN SC")
tooltip:AddSeparator(10)
end
if #item.sockets > 0 then
-- Sockets/links
local group = 0
local line = ""
for i, socket in ipairs(item.sockets) do
if i > 1 then
if socket.group == group then
line = line .. "^7="
else
line = line .. " "
end
group = socket.group
end
local code
if socket.color == "R" then
code = colorCodes.STRENGTH
elseif socket.color == "G" then
code = colorCodes.DEXTERITY
elseif socket.color == "B" then
code = colorCodes.INTELLIGENCE
elseif socket.color == "W" then
code = colorCodes.SCION
elseif socket.color == "A" then
code = "^xB0B0B0"
end
line = line .. code .. socket.color
end
tooltip:AddLine(fontSizeBig, "^x7F7F7FSockets: "..line, "FONTIN SC")
end
tooltip:AddSeparator(10)
if item.talismanTier then
tooltip:AddLine(fontSizeBig, "^x7F7F7FTalisman Tier ^xFFFFFF"..item.talismanTier, "FONTIN SC")
tooltip:AddSeparator(10)
end
-- Requirements
self.build:AddRequirementsToTooltip(tooltip, item.requirements.level,
item.requirements.strMod, item.requirements.dexMod, item.requirements.intMod,
item.requirements.str or 0, item.requirements.dex or 0, item.requirements.int or 0)
-- Modifiers
for _, modList in ipairs{item.enchantModLines, item.scourgeModLines, item.implicitModLines, item.explicitModLines, item.crucibleModLines} do
if modList[1] then
for _, modLine in ipairs(modList) do
if item:CheckModLineVariant(modLine) then
tooltip:AddLine(fontSizeBig, itemLib.formatModLine(modLine, dbMode), "FONTIN SC")
end
end
tooltip:AddSeparator(10)
end
end
-- Cluster jewel notables/keystone
if item.clusterJewel then
tooltip:AddSeparator(10)
if #item.jewelData.clusterJewelNotables > 0 then
for _, name in ipairs(item.jewelData.clusterJewelNotables) do
local node = self.build.spec.tree.clusterNodeMap[name]
if node then
tooltip:AddLine(fontSizeBig, colorCodes.MAGIC .. node.dn, "FONTIN SC")
for _, stat in ipairs(node.sd) do
tooltip:AddLine(fontSizeBig, "^x7F7F7F"..stat, "FONTIN SC")
end
end
end
elseif item.jewelData.clusterJewelKeystone then
local node = self.build.spec.tree.clusterNodeMap[item.jewelData.clusterJewelKeystone]
if node then
tooltip:AddLine(fontSizeBig, colorCodes.MAGIC .. node.dn, "FONTIN SC")
for _, stat in ipairs(node.sd) do
tooltip:AddLine(fontSizeBig, "^x7F7F7F"..stat, "FONTIN SC")
end
end
end
tooltip:AddSeparator(10)
end
-- Corrupted item label
if item.corrupted or item.split or item.mirrored then
if #item.explicitModLines == 0 then
tooltip:AddSeparator(10)
end
if item.split then
tooltip:AddLine(fontSizeBig, colorCodes.NEGATIVE.."Split", "FONTIN SC")
end
if item.mirrored then
tooltip:AddLine(fontSizeBig, colorCodes.NEGATIVE.."Mirrored", "FONTIN SC")
end
if item.corrupted then
tooltip:AddLine(fontSizeBig, colorCodes.NEGATIVE.."Corrupted", "FONTIN SC")
end
tooltip:AddSeparator(14)
end
-- Show flavour text:
if (item.rarity == "UNIQUE" or item.rarity == "RELIC" or item.baseName == "Grasping Mail") and main.showFlavourText == true then
local flavourTable = flavourLookup[item.baseName == "Grasping Mail" and item.baseName or item.title]
if flavourTable then
local flavour = nil
if item.title == "Grand Spectrum" then
local selectedFlavourId = nil
for _, lineEntry in ipairs(tooltip.lines or {}) do
local lineText = lineEntry.text or ""
if lineText:find("Power") then
selectedFlavourId = "UniqueJewel170"
break
elseif lineText:find("Endurance") then
selectedFlavourId = "UniqueJewel168"
break
elseif lineText:find("Frenzy") then
selectedFlavourId = "UniqueJewel169"
break
elseif lineText:find("Elemental Damage") then
selectedFlavourId = "UniqueJewel168"
break
elseif lineText:find("Elemental Ailments") then
selectedFlavourId = "UniqueJewel166"
break
elseif lineText:find("Elemental Resistances") or lineText:find("Armour") then
selectedFlavourId = "UniqueJewel76"
break
elseif lineText:find("Maximum Life") then
selectedFlavourId = "UniqueJewel165"
break
elseif lineText:find("Critical Strike Multiplier") then
selectedFlavourId = "UniqueJewel167"
break
elseif lineText:find("Critical Strike Chance") or lineText:find("Mana") then
selectedFlavourId = "UniqueJewel75"
break
end
end
if selectedFlavourId then
flavour = flavourTable[selectedFlavourId]
end
else
for _, text in pairs(flavourTable) do
flavour = text
break
end
end
if flavour then
for _, line in ipairs(flavour) do
tooltip:AddLine(fontSizeBig, colorCodes.UNIQUE .. line, "FONTIN SC ITALIC")
end
tooltip:AddSeparator(14)
end
end
end
-- Stat differences
if not self.showStatDifferences then
tooltip:AddSeparator(14)
tooltip:AddLine(14, colorCodes.TIP.."Tip: Press Ctrl+D to enable the display of stat differences.")
return
end
local calcFunc, calcBase = self.build.calcsTab:GetMiscCalculator()
if base.flask then
-- Special handling for flasks
local stats = { }
local flaskData = item.flaskData
local modDB = self.build.calcsTab.mainEnv.modDB
local output = self.build.calcsTab.mainOutput
local durInc = modDB:Sum("INC", nil, "FlaskDuration")
local effectInc = modDB:Sum("INC", { actor = "player" }, "FlaskEffect")
local lifeDur = 0
local manaDur = 0
if item.rarity == "MAGIC" and not item.base.flask.life and not item.base.flask.mana then
effectInc = effectInc + modDB:Sum("INC", { actor = "player" }, "MagicUtilityFlaskEffect")
end
if item.base.flask.life or item.base.flask.mana then
local rateInc = modDB:Sum("INC", nil, "FlaskRecoveryRate")
if item.base.flask.life then
local lifeInc = modDB:Sum("INC", nil, "FlaskLifeRecovery")
local lifeMore = modDB:More(nil, "FlaskLifeRecovery")
local lifeRateInc = modDB:Sum("INC", nil, "FlaskLifeRecoveryRate")
local instantPerc = flaskData.instantPerc + modDB:Sum("BASE", nil, "LifeFlaskInstantRecovery")
-- More life recovery while on low life is not affected by flask effect (verified ingame).
-- Since this will be multiplied by the flask effect value below we have to counteract this by removing the flask effect from the value beforehand.
-- This is also the reason why this value needs a separate multiplier and cannot just be calculated into FlaskLifeRecovery.
local lifeMoreOnLowLife = modDB:More(nil, "FlaskLifeRecoveryLowLife")
local lowLifeMulti = (lifeMoreOnLowLife > 1 and ((lifeMoreOnLowLife - 1) / (1 + effectInc / 100)) + 1 or 1)
local inst = flaskData.lifeBase * instantPerc / 100 * (1 + lifeInc / 100) * lifeMore * (1 + effectInc / 100) * lowLifeMulti
local base = flaskData.lifeBase * (1 - instantPerc / 100) * (1 + lifeInc / 100) * lifeMore * (1 + effectInc / 100) * (1 + durInc / 100) * lowLifeMulti
local grad = base * output.LifeRecoveryRateMod
local esGrad = base * output.EnergyShieldRecoveryRateMod
lifeDur = flaskData.duration * (1 + durInc / 100) / (1 + rateInc / 100) / (1 + lifeRateInc / 100)
-- LocalLifeFlaskAdditionalLifeRecovery flask mods
if flaskData.lifeAdditional > 0 and not self.build.configTab.input.conditionFullLife then
local totalAdditionalAmount = (flaskData.lifeAdditional/100) * flaskData.lifeTotal * output.LifeRecoveryRateMod
local additionalGrad = (lifeDur/10) * totalAdditionalAmount
local leftoverDur = 10 - lifeDur
local leftoverAmount = totalAdditionalAmount - additionalGrad
if inst > 0 then
if grad > 0 then
t_insert(stats, s_format("^8Life recovered: ^7%d ^8(^7%d^8 instantly, plus ^7%d ^8over^7 %.2fs^8, and an additional ^7%d ^8over subsequent ^7%.2fs^8)",
inst + grad + totalAdditionalAmount, inst, grad + additionalGrad, lifeDur, leftoverAmount, leftoverDur))
else
lifeDur = 0
t_insert(stats, s_format("^8Life recovered: ^7%d ^8(^7%d^8 instantly, and an additional ^7%d ^8over ^7%.2fs^8)",
inst + totalAdditionalAmount, inst, totalAdditionalAmount, 10))
end
else
t_insert(stats, s_format("^8Life recovered: ^7%d ^8(^7%d ^8over ^7%.2fs^8, and an additional ^7%d ^8over subsequent ^7%.2fs^8)",
grad + totalAdditionalAmount, grad + additionalGrad, lifeDur, leftoverAmount, leftoverDur))
end
else
if inst > 0 and grad > 0 then
t_insert(stats, s_format("^8Life recovered: ^7%d ^8(^7%d^8 instantly, plus ^7%d ^8over^7 %.2fs^8)", inst + grad, inst, grad, lifeDur))
-- modifiers to recovery amount or duration
elseif inst + grad ~= flaskData.lifeTotal or (inst == 0 and lifeDur ~= flaskData.duration) then
if inst > 0 then
lifeDur = 0
t_insert(stats, s_format("^8Life recovered: ^7%d ^8instantly", inst))
elseif grad > 0 then
t_insert(stats, s_format("^8Life recovered: ^7%d ^8over ^7%.2fs", grad, lifeDur))
end
end
end
if modDB:Flag(nil, "LifeFlaskAppliesToEnergyShield") then
if inst > 0 and esGrad > 0 then
t_insert(stats, s_format("^8Energy Shield recovered: ^7%d ^8(^7%d^8 instantly, plus ^7%d ^8over^7 %.2fs^8)", inst + esGrad, inst, esGrad, lifeDur))
elseif inst > 0 and esGrad == 0 then
t_insert(stats, s_format("^8Energy Shield recovered: ^7%d ^8instantly", inst))
elseif inst == 0 and esGrad > 0 then
t_insert(stats, s_format("^8Energy Shield recovered: ^7%d ^8over ^7%.2fs", esGrad, lifeDur))
end
end
end
if item.base.flask.mana then
local manaInc = modDB:Sum("INC", nil, "FlaskManaRecovery")
local manaRateInc = modDB:Sum("INC", nil, "FlaskManaRecoveryRate")
local instantPerc = flaskData.instantPerc + modDB:Sum("BASE", nil, "ManaFlaskInstantRecovery")
local inst = flaskData.manaBase * instantPerc / 100 * (1 + manaInc / 100) * (1 + effectInc / 100)
local base = flaskData.manaBase * (1 - instantPerc / 100) * (1 + manaInc / 100) * (1 + effectInc / 100) * (1 + durInc / 100)
local grad = base * output.ManaRecoveryRateMod
local lifeGrad = base * output.LifeRecoveryRateMod
manaDur = flaskData.duration * (1 + durInc / 100) / (1 + rateInc / 100) / (1 + manaRateInc / 100)
if inst > 0 and grad > 0 then
t_insert(stats, s_format("^8Mana recovered: ^7%d ^8(^7%d^8 instantly, plus ^7%d ^8over^7 %.2fs^8)", inst + grad, inst, grad, manaDur))
elseif inst + grad ~= flaskData.manaTotal or (inst == 0 and manaDur ~= flaskData.duration) then
if inst > 0 then
manaDur = 0
t_insert(stats, s_format("^8Mana recovered: ^7%d ^8instantly", inst))
elseif grad > 0 then
t_insert(stats, s_format("^8Mana recovered: ^7%d ^8over ^7%.2fs", grad, manaDur))
end
end
if modDB:Flag(nil, "ManaFlaskAppliesToLife") then
if lifeGrad > 0 then
t_insert(stats, s_format("^8Life recovered: ^7%d ^8over ^7%.2fs", lifeGrad, manaDur))
end
end
end
else
if durInc ~= 0 then
t_insert(stats, s_format("^8Flask effect duration: ^7%.1f0s", flaskData.duration * (1 + durInc / 100)))
end
end
local effectMod = 1 + (flaskData.effectInc + effectInc) / 100
if effectMod ~= 1 then
t_insert(stats, s_format("^8Flask effect modifier: ^7%+d%%", effectMod * 100 - 100))
end
local usedInc = modDB:Sum("INC", nil, "FlaskChargesUsed")
if usedInc ~= 0 then
local used = m_floor(flaskData.chargesUsed * (1 + usedInc / 100))
t_insert(stats, s_format("^8Charges used: ^7%d ^8of ^7%d ^8(^7%d ^8uses)", used, flaskData.chargesMax, m_floor(flaskData.chargesMax / used)))
end
local gainMod = flaskData.gainMod * (1 + modDB:Sum("INC", nil, "FlaskChargesGained") / 100)
if gainMod ~= 1 then
t_insert(stats, s_format("^8Charge gain modifier: ^7%+d%%", gainMod * 100 - 100))
end
-- charge generation
local chargesGenerated = modDB:Sum("BASE", nil, "FlaskChargesGenerated")
if item.base.flask.life then
chargesGenerated = chargesGenerated + modDB:Sum("BASE", nil, "LifeFlaskChargesGenerated")
end
if item.base.flask.mana then
chargesGenerated = chargesGenerated + modDB:Sum("BASE", nil, "ManaFlaskChargesGenerated")
end
if not item.base.flask.mana and not item.base.flask.life then
chargesGenerated = chargesGenerated + modDB:Sum("BASE", nil, "UtilityFlaskChargesGenerated")
end
local chargesGeneratedPerFlask = modDB:Sum("BASE", nil, "FlaskChargesGeneratedPerEmptyFlask")
local emptyFlaskSlots = 0
for slotName, slot in pairs(self.slots) do
if slotName:find("^Flask") ~= nil and slot.selItemId == 0 then
emptyFlaskSlots = emptyFlaskSlots + 1
end
end
chargesGeneratedPerFlask = chargesGeneratedPerFlask * emptyFlaskSlots
chargesGenerated = chargesGenerated * gainMod
chargesGeneratedPerFlask = chargesGeneratedPerFlask * gainMod
local totalChargesGenerated = chargesGenerated + chargesGeneratedPerFlask
if totalChargesGenerated > 0 then
t_insert(stats, s_format("^8Charges generated: ^7%.2f^8 per second", totalChargesGenerated))
end
local chanceToNotConsumeCharges = m_min(modDB:Sum("BASE", nil, "FlaskChanceNotConsumeCharges"), 100)
if chanceToNotConsumeCharges ~= 0 then
t_insert(stats, s_format("^8Chance to not consume charges: ^7%d%%", chanceToNotConsumeCharges))
end
-- flask uptime
local hasUptime = not item.base.flask.life and not item.base.flask.mana
local flaskDuration = flaskData.duration * (1 + durInc / 100)
if item.base.flask.life and (flaskData.lifeEffectNotRemoved or modDB:Flag(nil, "LifeFlaskEffectNotRemoved")) then
hasUptime = true
flaskDuration = lifeDur
elseif item.base.flask.mana and (flaskData.manaEffectNotRemoved or modDB:Flag(nil, "ManaFlaskEffectNotRemoved")) then
hasUptime = true
flaskDuration = manaDur
end
if hasUptime then
local flaskChargesUsed = flaskData.chargesUsed * (1 + usedInc / 100)
if flaskChargesUsed > 0 and flaskDuration > 0 then
local per3Duration = flaskDuration - (flaskDuration % 3)
local per5Duration = flaskDuration - (flaskDuration % 5)
local minimumChargesGenerated = per3Duration * chargesGenerated + per5Duration * chargesGeneratedPerFlask
local percentageMin = m_min(minimumChargesGenerated / flaskChargesUsed * 100, 100)
if percentageMin < 100 and chanceToNotConsumeCharges < 100 then
local averageChargesGenerated = (chargesGenerated + chargesGeneratedPerFlask) * flaskDuration
local averageChargesUsed = flaskChargesUsed * (100 - chanceToNotConsumeCharges) / 100
local percentageAvg = m_min(averageChargesGenerated / averageChargesUsed * 100, 100)
t_insert(stats, s_format("^8Flask uptime: ^7%d%%^8 average, ^7%d%%^8 minimum", percentageAvg, percentageMin))
else
t_insert(stats, s_format("^8Flask uptime: ^7100%%^8"))
end
end
end
if stats[1] then
tooltip:AddLine(14, "^7Effective flask stats:")
for _, stat in ipairs(stats) do
tooltip:AddLine(14, stat)
end
end
local output = calcFunc({ toggleFlask = item })
local header
if self.build.calcsTab.mainEnv.flasks[item] then
header = "^7Deactivating this flask will give you:"
else
header = "^7Activating this flask will give you:"
end
self.build:AddStatComparesToTooltip(tooltip, calcBase, output, header)
elseif base.tincture then
-- Special handling for tinctures
local stats = { }
local tinctureData = item.tinctureData
local modDB = self.build.calcsTab.mainEnv.modDB
local output = self.build.calcsTab.mainOutput
local effectInc = modDB:Sum("INC", { actor = "player" }, "TinctureEffect")
if item.rarity == "MAGIC" then
effectInc = effectInc + modDB:Sum("INC", { actor = "player" }, "MagicTinctureEffect")
end
local effectMod = (1 + (tinctureData.effectInc + effectInc) / 100) * (1 + (item.quality or 0) / 100)
if effectMod ~= 1 then
t_insert(stats, s_format("^8Tincture effect modifier: ^7%+d%%", effectMod * 100 - 100))
end
t_insert(stats, s_format("^8Mana Burn Inflicted Every Second: ^7%.2f", 1 / (tinctureData.manaBurn / (1 + modDB:Sum("INC", { actor = "player" }, "TinctureManaBurnRate") / 100) / (1 + modDB:Sum("MORE", { actor = "player" }, "TinctureManaBurnRate") / 100))))
local TincturesNotInflictManaBurn = m_min(modDB:Sum("BASE", nil, "TincturesNotInflictManaBurn"), 100)
if TincturesNotInflictManaBurn ~= 0 then
t_insert(stats, s_format("^8Chance to not inflict Mana Burn: ^7%d%%", TincturesNotInflictManaBurn))
end
t_insert(stats, s_format("^8Tincture Cooldown when deactivated: ^7%.2f^8 seconds", base.tincture.cooldown / (1 + (modDB:Sum("INC", { actor = "player" }, "TinctureCooldownRecovery") + tinctureData.cooldownInc) / 100)))
if stats[1] then
tooltip:AddLine(14, "^7Effective tincture stats:")
for _, stat in ipairs(stats) do
tooltip:AddLine(14, stat)
end
end
local output = calcFunc({ toggleTincture = item })
local header
if self.build.calcsTab.mainEnv.tinctures[item] then
header = "^7Deactivating this tincture will give you:"
else
header = "^7Activating this tincture will give you:"
end
self.build:AddStatComparesToTooltip(tooltip, calcBase, output, header)
else
self:UpdateSockets()
-- Build sorted list of slots to compare with
local compareSlots = { }
for slotName, slot in pairs(self.slots) do
if self:IsItemValidForSlot(item, slotName) and not slot.inactive and (not slot.weaponSet or slot.weaponSet == (self.activeItemSet.useSecondWeaponSet and 2 or 1)) then
t_insert(compareSlots, slot)
end
end
table.sort(compareSlots, function(a, b)
if a ~= b then
if slot == a then
return true
end
if slot == b then
return false
end
end
if a.selItemId ~= b.selItemId then
if item == self.items[a.selItemId] then
return true
end
if item == self.items[b.selItemId] then
return false
end
end
local aNum = tonumber(a.slotName:match("%d+"))
local bNum = tonumber(b.slotName:match("%d+"))
if aNum and bNum then
return aNum < bNum
else
return a.slotName < b.slotName
end
end)
-- Add comparisons for each slot
for _, compareSlot in pairs(compareSlots) do
if not main.slotOnlyTooltips or (slot and (slot.nodeId == compareSlot.nodeId or slot.slotName == compareSlot.slotName)) or not slot or slot == compareSlot then
local selItem = self.items[compareSlot.selItemId]
local output = calcFunc({ repSlotName = compareSlot.slotName, repItem = item ~= selItem and item or nil})
local header
if item == selItem then
header = "^7Removing this item from "..compareSlot.label.." will give you:"
else
header = string.format("^7Equipping this item in %s will give you:%s", compareSlot.label, selItem and "\n(replacing "..colorCodes[selItem.rarity]..selItem.name.."^7)" or "")
end
self.build:AddStatComparesToTooltip(tooltip, calcBase, output, header)
end
end
end
tooltip:AddLine(14, colorCodes.TIP.."Tip: Press Ctrl+D to disable the display of stat differences.")
if launch.devModeAlt then
-- Modifier debugging info
tooltip:AddSeparator(10)
for _, mod in ipairs(modList) do
tooltip:AddLine(14, "^7"..modLib.formatMod(mod))
end
end
end
function ItemsTabClass:CreateUndoState()
local state = { }
state.activeItemSetId = self.activeItemSetId
state.items = { }
for k, v in pairs(self.items) do
state.items[k] = copyTableSafe(self.items[k], true, true)
end
state.itemOrderList = copyTable(self.itemOrderList)
state.slotSelItemId = { }
for slotName, slot in pairs(self.slots) do
state.slotSelItemId[slotName] = slot.selItemId
end
state.itemSets = copyTableSafe(self.itemSets)
state.itemSetOrderList = copyTable(self.itemSetOrderList)
return state
end
function ItemsTabClass:RestoreUndoState(state)
self.items = state.items
wipeTable(self.itemOrderList)
for k, v in pairs(state.itemOrderList) do
self.itemOrderList[k] = v
end
for slotName, selItemId in pairs(state.slotSelItemId) do
self.slots[slotName]:SetSelItemId(selItemId)
end
self.itemSets = state.itemSets
wipeTable(self.itemSetOrderList)
for k, v in pairs(state.itemSetOrderList) do
self.itemSetOrderList[k] = v
end
self.activeItemSetId = state.activeItemSetId
self.activeItemSet = self.itemSets[self.activeItemSetId]
self:PopulateSlots()
end