Files
PathOfBuilding/src/Classes/Item.lua
2025-01-16 21:49:26 -06:00

1734 lines
66 KiB
Lua

-- Path of Building
--
-- Class: Item
-- Equippable item class
--
local ipairs = ipairs
local t_insert = table.insert
local t_remove = table.remove
local m_min = math.min
local m_max = math.max
local m_floor = math.floor
local dmgTypeList = {"Physical", "Lightning", "Cold", "Fire", "Chaos"}
local catalystList = {"Abrasive", "Accelerating", "Fertile", "Imbued", "Intrinsic", "Noxious", "Prismatic", "Tempering", "Turbulent", "Unstable"}
local catalystTags = {
{ "attack" },
{ "speed" },
{ "life", "mana", "resource" },
{ "caster" },
{ "jewellery_attribute", "attribute" },
{ "physical_damage", "chaos_damage" },
{ "jewellery_resistance", "resistance" },
{ "jewellery_defense", "defences" },
{ "jewellery_elemental" ,"elemental_damage" },
{ "critical" },
}
local function getCatalystScalar(catalystId, tags, quality)
if not catalystId or type(catalystId) ~= "number" or not catalystTags[catalystId] or not tags or type(tags) ~= "table" or #tags == 0 then
return 1
end
if not quality then
quality = 20
end
-- Create a fast lookup table for all provided tags
local tagLookup = {}
for _, curTag in ipairs(tags) do
tagLookup[curTag] = true;
end
-- Find if any of the catalyst's tags match the provided tags
for _, catalystTag in ipairs(catalystTags[catalystId]) do
if tagLookup[catalystTag] then
return (100 + quality) / 100
end
end
return 1
end
local influenceInfo = itemLib.influenceInfo
local ItemClass = newClass("Item", function(self, raw, rarity, highQuality)
if raw then
self:ParseRaw(sanitiseText(raw), rarity, highQuality)
end
end)
-- Reset all influence keys to false
function ItemClass:ResetInfluence()
for _, curInfluenceInfo in ipairs(influenceInfo) do
self[curInfluenceInfo.key] = false
end
end
local influenceItemMap = { }
for _, curInfluenceInfo in ipairs(influenceInfo) do
influenceItemMap[curInfluenceInfo.display.." Item"] = curInfluenceInfo.key
end
local lineFlags = {
["crafted"] = true, ["crucible"] = true, ["custom"] = true, ["eater"] = true, ["enchant"] = true,
["exarch"] = true, ["fractured"] = true, ["implicit"] = true, ["scourge"] = true, ["synthesis"] = true,
}
-- Special function to store unique instances of modifier on specific item slots
-- that require special handling for ItemConditions. Only called if line #224 is
-- uncommented
local specialModifierFoundList = {}
local inverseModifierFoundList = {}
local function getTagBasedModifiers(tagName, itemSlotName)
local tag_name = tagName:lower()
local slot_name = itemSlotName:lower():gsub(" ", "_")
-- iterate all the item modifiers
for k,v in pairs(data.itemMods.Item) do
-- iterate across the modifier tags for each modifier
for _,tag in ipairs(v.modTags) do
-- if tag matches the tag_name we are investigating
if tag:lower() == tag_name then
local found = false
-- if there is a valid weightKey table
if #v.weightKey > 0 then
for _,wk in ipairs(v.weightKey) do
-- and it matches the slot_name of the item we are investigating
if wk == slot_name then
for _, dv in ipairs(v) do
-- and the modifier description contains the tag_name keyword
if dv:lower():find(tag_name) then
found = true
break
else
local excluded = false
if data.itemTagSpecial[tagName] and data.itemTagSpecial[tagName][itemSlotName] then
for _, specialMod in ipairs(data.itemTagSpecial[tagName][itemSlotName]) do
if dv:lower():find(specialMod:lower()) then
exclude = true
break
end
end
end
if exclude then
found = true
break
end
end
end
if not found and not specialModifierFoundList[k] then
specialModifierFoundList[k] = true
ConPrintf("[%s] [%s] ENTRY: %s", tagName, itemSlotName, k)
end
end
end
else
for _, dv in ipairs(v) do
if dv:lower():find(tag_name) then
found = true
break
else
local excluded = false
if data.itemTagSpecial[tagName] and data.itemTagSpecial[tagName][itemSlotName] then
for _, specialMod in ipairs(data.itemTagSpecial[tagName][itemSlotName]) do
if dv:lower():find(specialMod:lower()) then
exclude = true
break
end
end
end
if exclude then
found = true
break
end
end
end
if not found and not specialModifierFoundList[k] then
specialModifierFoundList[k] = true
ConPrintf("[%s] ENTRY: %s", tagName, k)
end
end
end
end
for _, dv in ipairs(v) do
if dv:lower():find(tag_name) then
local found_2 = false
if #v.weightKey > 0 then
for _,wk in ipairs(v.weightKey) do
if wk == slot_name then
-- this is useless if the modTags = { } (is empty)
if #v.modTags > 0 then
for _,tag in ipairs(v.modTags) do
if tag:lower() == tag_name then
found_2 = true
break
else
local excluded = false
-- if we have an exclusion pattern list for that tagName and itemSlotName
if data.itemTagSpecialExclusionPattern[tagName] and data.itemTagSpecialExclusionPattern[tagName][itemSlotName] then
-- iterate across the exclusion patterns
for _, specialMod in ipairs(data.itemTagSpecialExclusionPattern[tagName][itemSlotName]) do
-- and if the description matches pattern exclude it
if dv:lower():find(specialMod:lower()) then
excluded = true
break
end
end
end
if excluded then
found_2 = true
break
end
end
end
if not found_2 and not inverseModifierFoundList[k] then
inverseModifierFoundList[k] = true
ConPrintf("[%s] appears in desc but not in tags. [%s] %s", tag_name, k, dv)
break
end
end
end
end
else
-- this is useless if the modTags = { } (is empty)
if #v.modTags > 0 then
for _,tag in ipairs(v.modTags) do
if tag:lower() == tag_name then
found_2 = true
break
else
local excluded = false
-- if we have an exclusion pattern list for that tagName and itemSlotName
if data.itemTagSpecialExclusionPattern[tagName] and data.itemTagSpecialExclusionPattern[tagName][itemSlotName] then
-- iterate across the exclusion patterns
for _, specialMod in ipairs(data.itemTagSpecialExclusionPattern[tagName][itemSlotName]) do
-- and if the description matches pattern exclude it
if dv:lower():find(specialMod:lower()) then
excluded = true
break
end
end
end
if excluded then
found_2 = true
break
end
end
end
if not found_2 and not inverseModifierFoundList[k] then
inverseModifierFoundList[k] = true
ConPrintf("[%s] appears in desc but not in tags. [%s] %s", tag_name, k, dv)
end
end
end
end
end
end
end
-- Iterate over modifiers to see if specific substring is found (for conditional checking)
function ItemClass:FindModifierSubstring(substring, itemSlotName)
local modLines = {}
local substring, explicit = substring:gsub("explicit ", "")
-- The commented out line below is used at GGPK updates to check if any new modifiers
-- have been identified that need to be added to the manually maintained special modifier
-- pool in Data.lua (data.itemTagSpecial and data.itemTagSpecialExclusionPattern tables)
--getTagBasedModifiers(substring, itemSlotName)
-- merge various modifier lines into one table
for _,v in pairs(self.explicitModLines) do t_insert(modLines, v) end
if explicit < 1 then
for _,v in pairs(self.enchantModLines) do t_insert(modLines, v) end
for _,v in pairs(self.scourgeModLines) do t_insert(modLines, v) end
for _,v in pairs(self.implicitModLines) do t_insert(modLines, v) end
for _,v in pairs(self.crucibleModLines) do t_insert(modLines, v) end
end
for _,v in pairs(modLines) do
local currentVariant = false
if v.variantList then
for variant, enabled in pairs(v.variantList) do
if enabled and variant == self.variant then
currentVariant = true
end
end
else
currentVariant = true
end
if currentVariant then
if v.line:lower():find(substring) and not v.line:lower():find(substring .. " modifier") then
local excluded = false
if data.itemTagSpecialExclusionPattern[substring] and data.itemTagSpecialExclusionPattern[substring][itemSlotName] then
for _, specialMod in ipairs(data.itemTagSpecialExclusionPattern[substring][itemSlotName]) do
if v.line:lower():find(specialMod:lower()) then
excluded = true
break
end
end
end
if not excluded then
return true
end
end
if data.itemTagSpecial[substring] and data.itemTagSpecial[substring][itemSlotName] then
for _, specialMod in ipairs(data.itemTagSpecial[substring][itemSlotName]) do
if v.line:lower():find(specialMod:lower()) and (not v.variantList or v.variantList[self.variant]) then
return true
end
end
end
end
end
return false
end
local function specToNumber(s)
local n = s:match("^([%+%-]?[%d%.]+)")
return n and tonumber(n)
end
-- Parse raw item data and extract item name, base type, quality, and modifiers
function ItemClass:ParseRaw(raw, rarity, highQuality)
self.raw = raw
self.name = "?"
self.namePrefix = ""
self.nameSuffix = ""
self.base = nil
self.rarity = rarity or "UNIQUE"
self.quality = nil
self.rawLines = { }
-- Find non-blank lines and trim whitespace
for line in raw:gmatch("%s*([^\n]*%S)") do
t_insert(self.rawLines, line)
end
local mode = rarity and "GAME" or "WIKI"
local l = 1
local itemClass
if self.rawLines[l] then
if self.rawLines[l]:match("^Item Class:") then
itemClass = self.rawLines[l]:gsub("^Item Class: %s+", "%1")
l = l + 1 -- Item class is already determined by the base type
end
local rarity = self.rawLines[l]:match("^Rarity: (%a+)")
if rarity then
mode = "GAME"
if colorCodes[rarity:upper()] then
self.rarity = rarity:upper()
end
if self.rarity == "UNIQUE" then
-- Hack for relics
for _, line in ipairs(self.rawLines) do
if line:find("Foil Unique") then
self.rarity = "RELIC"
break
end
end
end
l = l + 1
end
end
if self.rawLines[l] then
self.name = self.rawLines[l]
-- Determine if "Unidentified" item
local unidentified = false
for _, line in ipairs(self.rawLines) do
if line == "Unidentified" then
unidentified = true
break
end
end
-- Found the name for a rare or unique, but let's parse it if it's a magic or normal or Unidentified item to get the base
if not (self.rarity == "NORMAL" or self.rarity == "MAGIC" or unidentified) then
l = l + 1
end
end
self.checkSection = false
self.sockets = { }
self.classRequirementModLines = { }
self.buffModLines = { }
self.enchantModLines = { }
self.scourgeModLines = { }
self.implicitModLines = { }
self.explicitModLines = { }
self.crucibleModLines = { }
local implicitLines = 0
self.variantList = nil
self.prefixes = { }
self.suffixes = { }
self.requirements = { }
self.requirements.str = 0
self.requirements.dex = 0
self.requirements.int = 0
self.baseLines = { }
local importedLevelReq
local flaskBuffLines
local tinctureBuffLines
local deferJewelRadiusIndexAssignment
local gameModeStage = "FINDIMPLICIT"
local foundExplicit, foundImplicit
while self.rawLines[l] do
local line = self.rawLines[l]
if flaskBuffLines and flaskBuffLines[line] then
flaskBuffLines[line] = nil
elseif tinctureBuffLines and tinctureBuffLines[line] then
tinctureBuffLines[line] = nil
elseif line == "--------" then
self.checkSection = true
elseif line == "Split" then
self.split = true
elseif line == "Mirrored" then
self.mirrored = true
elseif line == "Corrupted" then
self.corrupted = true
elseif line == "Fractured Item" then
self.fractured = true
elseif line == "Synthesised Item" then
self.synthesised = true
elseif influenceItemMap[line] then
self[influenceItemMap[line]] = true
elseif line == "Requirements:" then
-- nothing to do
else
if self.checkSection then
if gameModeStage == "IMPLICIT" then
if foundImplicit then
-- There were definitely implicits, so any following modifiers must be explicits
gameModeStage = "EXPLICIT"
foundExplicit = true
else
gameModeStage = "FINDEXPLICIT"
end
elseif gameModeStage == "EXPLICIT" then
gameModeStage = "DONE"
elseif gameModeStage == "FINDIMPLICIT" and self.itemLevel and not line:match(" %(implicit%)") and
not line:match(" %(enchant%)") and not line:find("Talisman Tier") then
gameModeStage = "EXPLICIT"
foundExplicit = true
end
self.checkSection = false
end
local specName, specVal = line:match("^([%a ]+:?): (.+)$")
if specName then
if specName == "Class:" then
specName = "Requires Class"
end
else
specName, specVal = line:match("^(Requires %a+) (.+)$")
end
if specName then
if specName == "Unique ID" then
self.uniqueID = specVal
elseif specName == "Item Level" then
self.itemLevel = specToNumber(specVal)
elseif specName == "Requires Class" then
self.classRestriction = specVal
elseif specName == "Quality" then
self.quality = specToNumber(specVal)
elseif specName == "Sockets" then
local group = 0
for c in specVal:gmatch(".") do
if c:match("[RGBWA]") then
t_insert(self.sockets, { color = c, group = group })
elseif c == " " then
group = group + 1
end
end
elseif specName == "Radius" and self.type == "Jewel" then
self.jewelRadiusLabel = specVal:match("^%a+")
if specVal:match("^%a+") == "Variable" then
-- Jewel radius is variable and must be read from it's mods instead after they are parsed
deferJewelRadiusIndexAssignment = true
else
for index, data in pairs(data.jewelRadius) do
if specVal:match("^%a+") == data.label then
self.jewelRadiusIndex = index
break
end
end
end
elseif specName == "Limited to" and self.type == "Jewel" then
self.limit = specToNumber(specVal)
elseif specName == "Variant" then
if not self.variantList then
self.variantList = { }
end
-- This has to be kept for backwards compatibility
local ver, name = specVal:match("{([%w_]+)}(.+)")
if ver then
t_insert(self.variantList, name)
else
t_insert(self.variantList, specVal)
end
elseif specName == "Talisman Tier" then
self.talismanTier = specToNumber(specVal)
elseif specName == "Armour" or specName == "Evasion Rating" or specName == "Evasion" or specName == "Energy Shield" or specName == "Ward" then
if specName == "Evasion Rating" then
specName = "Evasion"
if self.baseName == "Two-Toned Boots (Armour/Energy Shield)" then
-- Another hack for Two-Toned Boots
self.baseName = "Two-Toned Boots (Armour/Evasion)"
self.base = data.itemBases[self.baseName]
end
elseif specName == "Energy Shield" then
specName = "EnergyShield"
if self.baseName == "Two-Toned Boots (Armour/Evasion)" then
-- Yet another hack for Two-Toned Boots
self.baseName = "Two-Toned Boots (Evasion/Energy Shield)"
self.base = data.itemBases[self.baseName]
end
end
self.armourData = self.armourData or { }
self.armourData[specName] = specToNumber(specVal)
elseif specName:match("BasePercentile") then
self.armourData = self.armourData or { }
self.armourData[specName] = specToNumber(specVal) or 0
elseif specName == "Requires Level" then
self.requirements.level = specToNumber(specVal)
elseif specName == "Level" then
-- Requirements from imported items can't always be trusted
importedLevelReq = specToNumber(specVal)
elseif specName == "LevelReq" then
self.requirements.level = specToNumber(specVal)
elseif specName == "Has Alt Variant" then
self.hasAltVariant = true
elseif specName == "Has Alt Variant Two" then
self.hasAltVariant2 = true
elseif specName == "Has Alt Variant Three" then
self.hasAltVariant3 = true
elseif specName == "Has Alt Variant Four" then
self.hasAltVariant4 = true
elseif specName == "Has Alt Variant Five" then
self.hasAltVariant5 = true
elseif specName == "Selected Variant" then
self.variant = specToNumber(specVal)
elseif specName == "Selected Alt Variant" then
self.variantAlt = specToNumber(specVal)
elseif specName == "Selected Alt Variant Two" then
self.variantAlt2 = specToNumber(specVal)
elseif specName == "Selected Alt Variant Three" then
self.variantAlt3 = specToNumber(specVal)
elseif specName == "Selected Alt Variant Four" then
self.variantAlt4 = specToNumber(specVal)
elseif specName == "Selected Alt Variant Five" then
self.variantAlt5 = specToNumber(specVal)
elseif specName == "Has Variants" or specName == "Selected Variants" then
-- Need to skip this line for backwards compatibility
-- with builds that used an old Watcher's Eye implementation
l = l + 1
elseif specName == "League" then
self.league = specVal
elseif specName == "Crafted" then
self.crafted = true
elseif specName == "Scourge" then
self.scourge = true
elseif specName == "Crucible" then
self.crucible = true
elseif specName == "Implicit" then
self.implicit = true
elseif specName == "Prefix" then
local range, affix = specVal:match("{range:([%d.]+)}(.+)")
range = range or ((affix or specVal) ~= "None" and main.defaultItemAffixQuality)
t_insert(self.prefixes, {
modId = affix or specVal,
range = tonumber(range),
})
elseif specName == "Suffix" then
local range, affix = specVal:match("{range:([%d.]+)}(.+)")
range = range or ((affix or specVal) ~= "None" and main.defaultItemAffixQuality)
t_insert(self.suffixes, {
modId = affix or specVal,
range = tonumber(range),
})
elseif specName == "Implicits" then
implicitLines = specToNumber(specVal) or 0
gameModeStage = "EXPLICIT"
elseif specName == "Unreleased" then
self.unreleased = (specVal == "true")
elseif specName == "Upgrade" then
self.upgradePaths = self.upgradePaths or { }
t_insert(self.upgradePaths, specVal)
elseif specName == "Source" then
self.source = specVal
elseif specName == "Cluster Jewel Skill" then
if self.clusterJewel and self.clusterJewel.skills[specVal] then
self.clusterJewelSkill = specVal
end
elseif specName == "Cluster Jewel Node Count" then
if self.clusterJewel then
local num = specToNumber(specVal) or self.clusterJewel.maxNodes
self.clusterJewelNodeCount = m_min(m_max(num, self.clusterJewel.minNodes), self.clusterJewel.maxNodes)
end
elseif specName == "Catalyst" then
for i=1, #catalystList do
if specVal == catalystList[i] then
self.catalyst = i
end
end
elseif specName == "CatalystQuality" then
self.catalystQuality = specToNumber(specVal)
elseif specName == "Note" then
self.note = specVal
elseif specName == "Str" or specName == "Strength" or specName == "Dex" or specName == "Dexterity" or
specName == "Int" or specName == "Intelligence" then
self.requirements[specName:sub(1,3):lower()] = specToNumber(specVal)
elseif specName == "Critical Strike Range" or specName == "Attacks per Second" or specName == "Weapon Range" or
specName == "Critical Strike Chance" or specName == "Physical Damage" or specName == "Elemental Damage" or
specName == "Chaos Damage" or specName == "Chance to Block" or specName == "Block chance" or
specName == "Armour" or specName == "Energy Shield" or specName == "Evasion" then
self.hidden_specs = true
-- Anything else is an explicit with a colon in it (Fortress Covenant, Pure Talent, etc) unless it's part of the custom name
elseif not (self.name:match(specName) and self.name:match(specVal)) then
foundExplicit = true
gameModeStage = "EXPLICIT"
end
end
if line == "Prefixes:" then
foundExplicit = true
gameModeStage = "EXPLICIT"
end
if not specName or foundExplicit or foundImplicit then
local modLine = { modTags = {} }
line = line:gsub("{(%a*):?([^}]*)}", function(k,val)
if k == "variant" then
modLine.variantList = { }
for varId in val:gmatch("%d+") do
modLine.variantList[tonumber(varId)] = true
end
elseif k == "tags" then
for tag in val:gmatch("[%a_]+") do
t_insert(modLine.modTags, tag)
end
elseif k == "range" then
modLine.range = tonumber(val)
elseif lineFlags[k] then
modLine[k] = true
end
return ""
end)
line = line:gsub(" %((%l+)%)", function(k)
if lineFlags[k] then
modLine[k] = true
end
return ""
end)
if modLine.enchant then
modLine.crafted = true
modLine.implicit = true
end
local baseName
if not self.base and (self.rarity == "NORMAL" or self.rarity == "MAGIC") then
-- Exact match (affix-less magic and normal items)
if self.name:match("Energy Blade") and itemClass then -- Special handling for energy blade base.
self.name = itemClass:match("One Hand") and "Energy Blade One Handed" or "Energy Blade Two Handed"
end
if data.itemBases[self.name] then
baseName = self.name
else
local bestMatch = {length = -1}
-- Partial match (magic items with affixes)
for itemBaseName, baseData in pairs(data.itemBases) do
local s, e = self.name:find(itemBaseName, 1, true)
if s and e and (e-s > bestMatch.length) then
bestMatch.match = itemBaseName
bestMatch.length = e-s
bestMatch.e = e
bestMatch.s = s
end
end
if bestMatch.match then
self.namePrefix = self.name:sub(1, bestMatch.s - 1)
self.nameSuffix = self.name:sub(bestMatch.e + 1)
baseName = bestMatch.match
end
end
if not baseName then
local s, e = self.name:find("Two-Toned Boots", 1, true)
if s then
-- Hack for Two-Toned Boots
baseName = "Two-Toned Boots"
self.namePrefix = self.name:sub(1, s - 1)
self.nameSuffix = self.name:sub(e + 1)
end
end
self.name = self.name:gsub(" %(.+%)","")
end
if not baseName then
baseName = line:gsub("^Superior ", ""):gsub("^Synthesised ","")
end
if baseName == "Two-Toned Boots" then
baseName = "Two-Toned Boots (Armour/Energy Shield)"
end
local base = data.itemBases[baseName]
if base then
-- Items with variants can have multiple bases
self.baseLines[baseName] = { line = baseName, variantList = modLine.variantList }
-- Set the actual base if variant matches or doesn't have variants
if not self.variant or not modLine.variantList or modLine.variantList[self.variant] then
self.baseName = baseName
if not (self.rarity == "NORMAL" or self.rarity == "MAGIC") then
self.title = self.name
end
self.type = base.type
self.base = base
self.affixes = (self.base.subType and data.itemMods[self.base.type..self.base.subType])
or data.itemMods[self.base.type]
or data.itemMods.Item
if self.base.flask then
if self.base.utility_flask then
self.enchantments = data.enchantments["UtilityFlask"]
else
self.enchantments = data.enchantments["Flask"]
end
else
self.enchantments = data.enchantments[self.base.type]
end
self.corruptible = self.base.type ~= "Flask"
self.canBeInfluenced = self.base.influenceTags ~= nil
self.clusterJewel = data.clusterJewels and data.clusterJewels.jewels[self.baseName]
self.requirements.str = self.base.req.str or 0
self.requirements.dex = self.base.req.dex or 0
self.requirements.int = self.base.req.int or 0
local maxReq = m_max(self.requirements.str, self.requirements.dex, self.requirements.int)
self.defaultSocketColor = (maxReq == self.requirements.dex and "G") or (maxReq == self.requirements.int and "B") or "R"
if self.base.flask and self.base.flask.buff and not flaskBuffLines then
flaskBuffLines = { }
for _, line in ipairs(self.base.flask.buff) do
flaskBuffLines[line] = true
local modList, extra = modLib.parseMod(line)
t_insert(self.buffModLines, { line = line, extra = extra, modList = modList or { } })
end
end
if self.base.tincture and self.base.tincture.buff and not tinctureBuffLines then
tinctureBuffLines = { }
for _, line in ipairs(self.base.tincture.buff) do
tinctureBuffLines[line] = true
local modList, extra = modLib.parseMod(line)
t_insert(self.buffModLines, { line = line, extra = extra, modList = modList or { } })
end
end
end
-- Base lines don't need mod parsing, skip it
goto continue
end
if modLine.implicit then
foundImplicit = true
gameModeStage = "IMPLICIT"
end
local catalystScalar = getCatalystScalar(self.catalyst, modLine.modTags, self.catalystQuality)
local rangedLine = itemLib.applyRange(line, 1, catalystScalar)
local modList, extra = modLib.parseMod(rangedLine)
if (not modList or extra) and self.rawLines[l+1] then
-- Try to combine it with the next line
local nextLine = self.rawLines[l+1]:gsub("%b{}", ""):gsub(" ?%(%l+%)","")
local combLine = line.." "..nextLine
rangedLine = itemLib.applyRange(combLine, 1, catalystScalar)
modList, extra = modLib.parseMod(rangedLine, true)
if modList and not extra then
line = line.."\n"..nextLine
l = l + 1
else
modList, extra = modLib.parseMod(rangedLine)
end
end
local lineLower = line:lower()
if lineLower == "implicit modifiers cannot be changed" then
self.implicitsCannotBeChanged = true
elseif lineLower:match(" prefix modifiers? allowed") then
self.prefixes.limit = (self.prefixes.limit or 0) + (tonumber(lineLower:match("%+(%d+) prefix modifiers? allowed")) or 0) - (tonumber(lineLower:match("%-(%d+) prefix modifiers? allowed")) or 0)
elseif lineLower:match(" suffix modifiers? allowed") then
self.suffixes.limit = (self.suffixes.limit or 0) + (tonumber(lineLower:match("%+(%d+) suffix modifiers? allowed")) or 0) - (tonumber(lineLower:match("%-(%d+) suffix modifiers? allowed")) or 0)
elseif lineLower == "this item can be anointed by cassia" then
self.canBeAnointed = true
elseif lineLower == "can have a second enchantment modifier" then
self.canHaveTwoEnchants = true
elseif lineLower == "can have 1 additional enchantment modifiers" then
self.canHaveTwoEnchants = true
elseif lineLower == "can have 2 additional enchantment modifiers" then
self.canHaveTwoEnchants = true
self.canHaveThreeEnchants = true
elseif lineLower == "can have 3 additional enchantment modifiers" then
self.canHaveTwoEnchants = true
self.canHaveThreeEnchants = true
self.canHaveFourEnchants = true
elseif lineLower == "has a crucible passive skill tree with only support passive skills" then
self.canHaveOnlySupportSkillsCrucibleTree = true
elseif lineLower == "has a crucible passive skill tree" then
self.canHaveShieldCrucibleTree = true
elseif lineLower == "has a two handed sword crucible passive skill tree" then
self.canHaveTwoHandedSwordCrucibleTree = true
end
local modLines
if modLine.enchant or (modLine.crafted and #self.enchantModLines + #self.implicitModLines < implicitLines) then
modLines = self.enchantModLines
elseif modLine.scourge then
modLines = self.scourgeModLines
elseif line:find("Requires Class") then
modLines = self.classRequirementModLines
elseif modLine.implicit or (not modLine.crafted and #self.enchantModLines + #self.scourgeModLines + #self.implicitModLines < implicitLines) then
modLines = self.implicitModLines
elseif modLine.crucible then
modLines = self.crucibleModLines
else
modLines = self.explicitModLines
end
modLine.line = line
if modList then
modLine.modList = modList
modLine.extra = extra
modLine.valueScalar = catalystScalar
modLine.range = modLine.range or main.defaultItemAffixQuality
t_insert(modLines, modLine)
if mode == "GAME" then
if gameModeStage == "FINDIMPLICIT" then
gameModeStage = "IMPLICIT"
elseif gameModeStage == "FINDEXPLICIT" then
foundExplicit = true
gameModeStage = "EXPLICIT"
elseif gameModeStage == "EXPLICIT" then
foundExplicit = true
end
else
foundExplicit = true
end
elseif mode == "GAME" then
if gameModeStage == "IMPLICIT" or gameModeStage == "EXPLICIT" or (gameModeStage == "FINDIMPLICIT" and (not data.itemBases[line]) and not (self.name == line) and not line:find("Two%-Toned") and not (self.base and (line == self.base.type or self.base.subType and line == self.base.subType .. " " .. self.base.type))) then
modLine.modList = { }
modLine.extra = line
t_insert(modLines, modLine)
elseif gameModeStage == "FINDEXPLICIT" then
gameModeStage = "DONE"
end
elseif foundExplicit then
modLine.modList = { }
modLine.extra = line
t_insert(modLines, modLine)
end
end
end
::continue::
l = l + 1
end
if self.baseName and self.title then
self.name = self.title .. ", " .. self.baseName:gsub(" %(.+%)","")
end
if self.base and not self.requirements.level then
if importedLevelReq and #self.sockets == 0 then
-- Requirements on imported items can only be trusted for items with no sockets
self.requirements.level = importedLevelReq
else
self.requirements.level = self.base.req.level
end
end
self.affixLimit = 0
if self.crafted then
if not self.affixes then
self.crafted = false
elseif self.rarity == "MAGIC" then
if self.prefixes.limit or self.suffixes.limit then
self.prefixes.limit = m_max(m_min((self.prefixes.limit or 0) + 1, 2), 0)
self.suffixes.limit = m_max(m_min((self.suffixes.limit or 0) + 1, 2), 0)
self.affixLimit = self.prefixes.limit + self.suffixes.limit
else
self.affixLimit = 2
end
elseif self.rarity == "RARE" then
self.affixLimit = ((self.type == "Jewel" and not (self.base.subType == "Abyss" and self.corrupted)) and 4 or 6)
if self.prefixes.limit or self.suffixes.limit then
self.prefixes.limit = m_max(m_min((self.prefixes.limit or 0) + self.affixLimit / 2, self.affixLimit), 0)
self.suffixes.limit = m_max(m_min((self.suffixes.limit or 0) + self.affixLimit / 2, self.affixLimit), 0)
self.affixLimit = self.prefixes.limit + self.suffixes.limit
end
else
self.crafted = false
end
if self.crafted then
for _, list in ipairs({self.prefixes,self.suffixes}) do
for i = 1, (list.limit or (self.affixLimit / 2)) do
if not list[i] then
list[i] = { modId = "None" }
elseif list[i].modId ~= "None" and not self.affixes[list[i].modId] then
for modId, mod in pairs(self.affixes) do
if list[i].modId == mod.affix then
list[i].modId = modId
break
end
end
if not self.affixes[list[i].modId] then
list[i].modId = "None"
end
end
end
end
end
end
if self.base and self.base.socketLimit then
if #self.sockets == 0 then
for i = 1, self.base.socketLimit do
t_insert(self.sockets, {
color = self.defaultSocketColor,
group = 0,
})
end
end
end
self.abyssalSocketCount = 0
if self.variantList then
self.variant = m_min(#self.variantList, self.variant or #self.variantList)
if self.hasAltVariant then
self.variantAlt = m_min(#self.variantList, self.variantAlt or #self.variantList)
end
if self.hasAltVariant2 then
self.variantAlt2 = m_min(#self.variantList, self.variantAlt2 or #self.variantList)
end
if self.hasAltVariant3 then
self.variantAlt3 = m_min(#self.variantList, self.variantAlt3 or #self.variantList)
end
if self.hasAltVariant4 then
self.variantAlt4 = m_min(#self.variantList, self.variantAlt4 or #self.variantList)
end
if self.hasAltVariant5 then
self.variantAlt5 = m_min(#self.variantList, self.variantAlt5 or #self.variantList)
end
end
if not self.quality then
self:NormaliseQuality()
if highQuality then
-- Behavior of NormaliseQuality should be looked at because calling it twice has different results.
-- Leaving it alone for now. Just moving it here from Main.lua so BuildAndParseRaw doesn't need to be called.
self:NormaliseQuality()
end
end
self:BuildModList()
if deferJewelRadiusIndexAssignment then
self.jewelRadiusIndex = self.jewelData.radiusIndex
end
end
function ItemClass:NormaliseQuality()
if self.base and (self.base.armour or self.base.weapon or self.base.flask or self.base.tincture) then
if not self.quality then
self.quality = 0
elseif not self.uniqueID and not self.corrupted and not self.split and not self.mirrored and self.quality < 20 then
self.quality = 20
end
end
end
function ItemClass:GetModSpawnWeight(mod, includeTags, excludeTags)
local weight = 0
if self.base then
local function HasInfluenceTag(key)
if self.base.influenceTags then
for _, curInfluenceInfo in ipairs(influenceInfo) do
if self[curInfluenceInfo.key] and self.base.influenceTags[curInfluenceInfo.key] == key then
return true
end
end
end
return false
end
local function HasMavenInfluence(modAffix)
return modAffix:match("Elevated")
end
for i, key in ipairs(mod.weightKey) do
if (self.base.tags[key] or (includeTags and includeTags[key]) or HasInfluenceTag(key)) and not (excludeTags and excludeTags[key]) then
weight = (HasInfluenceTag(key) and HasMavenInfluence(mod.affix)) and 1000 or mod.weightVal[i]
break
end
end
for i, key in ipairs(mod.weightMultiplierKey or {}) do
if (self.base.tags[key] or (includeTags and includeTags[key]) or HasInfluenceTag(key)) and not (excludeTags and excludeTags[key]) then
weight = weight * mod.weightMultiplierVal[i] / 100
break
end
end
end
return weight
end
function ItemClass:GetNecropolisModSpawnWeight(mod)
local weight = 0
if self.base then
for i, key in ipairs(mod.weightKey) do
if self.base.tags[key:gsub("necropolis_", "")] then
weight = mod.weightVal[i]
break
end
end
end
return weight
end
function ItemClass:CheckIfModIsDelve(mod)
return mod.affix == "Subterranean" or mod.affix == "of the Underground"
end
function ItemClass:BuildRaw()
local rawLines = { }
t_insert(rawLines, "Rarity: " .. self.rarity)
if self.title then
t_insert(rawLines, self.title)
t_insert(rawLines, self.baseName)
else
t_insert(rawLines, (self.namePrefix or "") .. self.baseName .. (self.nameSuffix or ""))
end
if self.armourData then
for _, type in ipairs({ "Armour", "Evasion", "EnergyShield", "Ward" }) do
if self.armourData[type] and self.armourData[type] > 0 then
t_insert(rawLines, type:gsub("EnergyShield", "Energy Shield") .. ": " .. self.armourData[type])
if self.armourData[type .. "BasePercentile"] then
t_insert(rawLines, type .. "BasePercentile: " .. self.armourData[type .. "BasePercentile"])
end
end
end
end
if self.uniqueID then
t_insert(rawLines, "Unique ID: " .. self.uniqueID)
end
if self.league then
t_insert(rawLines, "League: " .. self.league)
end
if self.unreleased then
t_insert(rawLines, "Unreleased: true")
end
for i, curInfluenceInfo in ipairs(influenceInfo) do
if self[curInfluenceInfo.key] then
t_insert(rawLines, curInfluenceInfo.display .. " Item")
end
end
if self.crafted then
t_insert(rawLines, "Crafted: true")
for i, affix in ipairs(self.prefixes or { }) do
t_insert(rawLines, "Prefix: " .. (affix.range and ("{range:" .. round(affix.range,3) .. "}") or "") .. affix.modId)
end
for i, affix in ipairs(self.suffixes or { }) do
t_insert(rawLines, "Suffix: " .. (affix.range and ("{range:" .. round(affix.range,3) .. "}") or "") .. affix.modId)
end
end
if self.catalyst and self.catalyst > 0 then
t_insert(rawLines, "Catalyst: " .. catalystList[self.catalyst])
end
if self.catalystQuality then
t_insert(rawLines, "CatalystQuality: " .. self.catalystQuality)
end
if self.clusterJewel then
if self.clusterJewelSkill then
t_insert(rawLines, "Cluster Jewel Skill: " .. self.clusterJewelSkill)
end
if self.clusterJewelNodeCount then
t_insert(rawLines, "Cluster Jewel Node Count: " .. self.clusterJewelNodeCount)
end
end
if self.talismanTier then
t_insert(rawLines, "Talisman Tier: " .. self.talismanTier)
end
if self.itemLevel then
t_insert(rawLines, "Item Level: " .. self.itemLevel)
end
local function writeModLine(modLine)
local line = modLine.line
if modLine.range and line:match("%(%-?[%d%.]+%-%-?[%d%.]+%)") then
line = "{range:" .. round(modLine.range, 3) .. "}" .. line
end
if modLine.crafted then
line = "{crafted}" .. line
end
if modLine.custom then
line = "{custom}" .. line
end
if modLine.scourge then
line = "{scourge}" .. line
end
if modLine.crucible then
line = "{crucible}" .. line
end
if modLine.fractured then
line = "{fractured}" .. line
end
if modLine.exarch then
line = "{exarch}" .. line
end
if modLine.eater then
line = "{eater}" .. line
end
if modLine.synthesis then
line = "{synthesis}" .. line
end
if modLine.variantList then
local varSpec
for varId in pairs(modLine.variantList) do
varSpec = (varSpec and varSpec .. "," or "") .. varId
end
line = "{variant:" .. varSpec .. "}" .. line
end
if modLine.modTags and #modLine.modTags > 0 then
line = "{tags:" .. table.concat(modLine.modTags, ",") .. "}" .. line
end
t_insert(rawLines, line)
end
if self.variantList then
for _, variantName in ipairs(self.variantList) do
t_insert(rawLines, "Variant: " .. variantName)
end
t_insert(rawLines, "Selected Variant: " .. self.variant)
for _, baseLine in pairs(self.baseLines) do
if baseLine.variantList then
writeModLine(baseLine)
end
end
if self.hasAltVariant then
t_insert(rawLines, "Has Alt Variant: true")
t_insert(rawLines, "Selected Alt Variant: " .. self.variantAlt)
end
if self.hasAltVariant2 then
t_insert(rawLines, "Has Alt Variant Two: true")
t_insert(rawLines, "Selected Alt Variant Two: " .. self.variantAlt2)
end
if self.hasAltVariant3 then
t_insert(rawLines, "Has Alt Variant Three: true")
t_insert(rawLines, "Selected Alt Variant Three: " .. self.variantAlt3)
end
if self.hasAltVariant4 then
t_insert(rawLines, "Has Alt Variant Four: true")
t_insert(rawLines, "Selected Alt Variant Four: " .. self.variantAlt4)
end
if self.hasAltVariant5 then
t_insert(rawLines, "Has Alt Variant Five: true")
t_insert(rawLines, "Selected Alt Variant Five: " .. self.variantAlt5)
end
end
if self.quality then
t_insert(rawLines, "Quality: " .. self.quality)
end
if self.sockets and #self.sockets > 0 then
local line = "Sockets: "
for i, socket in pairs(self.sockets) do
line = line .. socket.color
if self.sockets[i+1] then
line = line .. (socket.group == self.sockets[i+1].group and "-" or " ")
end
end
t_insert(rawLines, line)
end
if self.requirements and self.requirements.level then
t_insert(rawLines, "LevelReq: " .. self.requirements.level)
end
if self.jewelRadiusLabel then
t_insert(rawLines, "Radius: " .. self.jewelRadiusLabel)
end
if self.limit then
t_insert(rawLines, "Limited to: " .. self.limit)
end
if self.classRestriction then
t_insert(rawLines, "Requires Class " .. self.classRestriction)
end
t_insert(rawLines, "Implicits: " .. (#self.enchantModLines + #self.implicitModLines + #self.scourgeModLines))
for _, modLine in ipairs(self.enchantModLines) do
writeModLine(modLine)
end
for _, modLine in ipairs(self.scourgeModLines) do
writeModLine(modLine)
end
for _, modLine in ipairs(self.classRequirementModLines) do
writeModLine(modLine)
end
for _, modLine in ipairs(self.implicitModLines) do
writeModLine(modLine)
end
for _, modLine in ipairs(self.explicitModLines) do
writeModLine(modLine)
end
for _, modLine in ipairs(self.crucibleModLines) do
writeModLine(modLine)
end
if self.split then
t_insert(rawLines, "Split")
end
if self.mirrored then
t_insert(rawLines, "Mirrored")
end
if self.corrupted or self.scourge then
t_insert(rawLines, "Corrupted")
end
return table.concat(rawLines, "\n")
end
function ItemClass:BuildAndParseRaw()
local raw = self:BuildRaw()
self:ParseRaw(raw)
end
-- Rebuild explicit modifiers using the item's affixes
function ItemClass:Craft()
-- Save off any crafted or custom mods so they can be re-added at the end
local savedMods = {}
for _, mod in ipairs(self.explicitModLines) do
if mod.crafted or mod.custom then
t_insert(savedMods, mod)
end
end
wipeTable(self.explicitModLines)
self.namePrefix = ""
self.nameSuffix = ""
self.requirements.level = self.base.req.level
local statOrder = { }
for _, list in ipairs({self.prefixes,self.suffixes}) do
for i = 1, (list.limit or (self.affixLimit / 2)) do
local affix = list[i]
if not affix then
list[i] = { modId = "None" }
end
local mod = self.affixes[affix.modId]
if mod then
if mod.type == "Prefix" then
self.namePrefix = mod.affix .. " " .. self.namePrefix
elseif mod.type == "Suffix" then
self.nameSuffix = self.nameSuffix .. " " .. mod.affix
end
self.requirements.level = m_max(self.requirements.level or 0, m_floor(mod.level * 0.8))
local rangeScalar = getCatalystScalar(self.catalyst, mod.modTags, self.catalystQuality)
for i, line in ipairs(mod) do
line = itemLib.applyRange(line, affix.range or 0.5, rangeScalar)
local order = mod.statOrder[i]
if statOrder[order] then
-- Combine stats
local start = 1
statOrder[order].line = statOrder[order].line:gsub("%d+", function(num)
local s, e, other = line:find("(%d+)", start)
start = e + 1
return tonumber(num) + tonumber(other)
end)
else
local modLine = { line = line, order = order }
for l = 1, #self.explicitModLines + 1 do
if not self.explicitModLines[l] or self.explicitModLines[l].order > order then
t_insert(self.explicitModLines, l, modLine)
break
end
end
statOrder[order] = modLine
end
end
end
end
end
-- Restore the crafted and custom mods
for _, mod in ipairs(savedMods) do
t_insert(self.explicitModLines, mod)
end
self:BuildAndParseRaw()
end
function ItemClass:CheckModLineVariant(modLine)
return not modLine.variantList
or modLine.variantList[self.variant]
or (self.hasAltVariant and modLine.variantList[self.variantAlt])
or (self.hasAltVariant2 and modLine.variantList[self.variantAlt2])
or (self.hasAltVariant3 and modLine.variantList[self.variantAlt3])
or (self.hasAltVariant4 and modLine.variantList[self.variantAlt4])
or (self.hasAltVariant5 and modLine.variantList[self.variantAlt5])
end
-- Return the name of the slot this item is equipped in
function ItemClass:GetPrimarySlot()
if self.base.weapon then
return "Weapon 1"
elseif self.type == "Quiver" or self.type == "Shield" then
return "Weapon 2"
elseif self.type == "Ring" then
return "Ring 1"
elseif self.type == "Flask" then
return "Flask 1"
else
return self.type
end
end
-- Calculate local modifiers, and removes them from the modifier list
-- To be considered local, a modifier must be an exact flag match, and cannot have any tags (e.g. conditions, multipliers)
-- Only the InSlot tag is allowed (for Adds x to x X Damage in X Hand modifiers)
local function calcLocal(modList, name, type, flags)
local result
if type == "FLAG" then
result = false
elseif type == "MORE" then
result = 1
else
result = 0
end
local i = 1
while modList[i] do
local mod = modList[i]
if mod.name == name and mod.type == type and mod.flags == flags and mod.keywordFlags == 0 and (not mod[1] or mod[1].type == "InSlot") then
if type == "FLAG" then
result = result or mod.value
-- convert MORE to times multiplier, e.g. 50% more = 1.5x, result = 1.5
elseif type == "MORE" then
result = result * ((100 + mod.value) / 100)
else
result = result + mod.value
end
t_remove(modList, i)
else
i = i + 1
end
end
return result
end
-- Build list of modifiers in a given slot number (1 or 2) while applying local modifiers and adding quality
function ItemClass:BuildModListForSlotNum(baseList, slotNum)
local slotName = self:GetPrimarySlot()
if slotNum == 2 then
slotName = slotName:gsub("1", "2")
end
local modList = new("ModList")
for _, baseMod in ipairs(baseList) do
local mod = copyTable(baseMod)
local add = true
for _, tag in ipairs(mod) do
if tag.type == "SlotNumber" or tag.type == "InSlot" then
if tag.num ~= slotNum then
add = false
break
end
end
for k, v in pairs(tag) do
if type(v) == "string" then
tag[k] = v:gsub("{SlotName}", slotName)
:gsub("{Hand}", (slotNum == 1) and "MainHand" or "OffHand")
:gsub("{OtherSlotNum}", slotNum == 1 and "2" or "1")
end
end
end
if add then
mod.sourceSlot = slotName
modList:AddMod(mod)
end
end
if #self.sockets > 0 then
local multiName = {
R = "Multiplier:RedSocketIn"..slotName,
G = "Multiplier:GreenSocketIn"..slotName,
B = "Multiplier:BlueSocketIn"..slotName,
W = "Multiplier:WhiteSocketIn"..slotName,
}
for _, socket in ipairs(self.sockets) do
if multiName[socket.color] then
modList:NewMod(multiName[socket.color], "BASE", 1, "Item Sockets")
end
end
end
local craftedQuality = calcLocal(modList,"Quality","BASE",0) or 0
if craftedQuality ~= self.craftedQuality then
if self.craftedQuality then
self.quality = (self.quality or 0) - self.craftedQuality + craftedQuality
end
self.craftedQuality = craftedQuality
end
if self.quality then
modList:NewMod("Multiplier:QualityOn"..slotName, "BASE", self.quality, "Quality")
end
if self.base.weapon then
local weaponData = { }
self.weaponData[slotNum] = weaponData
weaponData.type = self.base.type
weaponData.name = self.name
weaponData.AttackSpeedInc = calcLocal(modList, "Speed", "INC", ModFlag.Attack) + m_floor(self.quality / 8 * calcLocal(modList, "AlternateQualityLocalAttackSpeedPer8Quality", "INC", 0))
weaponData.AttackRate = round(self.base.weapon.AttackRateBase * (1 + weaponData.AttackSpeedInc / 100), 2)
weaponData.rangeBonus = calcLocal(modList, "WeaponRange", "BASE", 0) + 10 * calcLocal(modList, "WeaponRangeMetre", "BASE", 0) + m_floor(self.quality / 10 * calcLocal(modList, "AlternateQualityLocalWeaponRangePer10Quality", "BASE", 0))
weaponData.range = self.base.weapon.Range + weaponData.rangeBonus
local LocalIncEle = calcLocal(modList, "LocalElementalDamage", "INC", 0)
for _, dmgType in pairs(dmgTypeList) do
local min = (self.base.weapon[dmgType.."Min"] or 0) + calcLocal(modList, dmgType.."Min", "BASE", 0)
local max = (self.base.weapon[dmgType.."Max"] or 0) + calcLocal(modList, dmgType.."Max", "BASE", 0)
if dmgType == "Physical" then
local physInc = calcLocal(modList, "PhysicalDamage", "INC", 0)
local qualityScalar = self.quality
if calcLocal(modList, "AlternateQualityWeapon", "BASE", 0) > 0 then
qualityScalar = 0
end
min = round(min * (1 + physInc / 100) * (1 + qualityScalar / 100))
max = round(max * (1 + physInc / 100) * (1 + qualityScalar / 100))
elseif dmgType ~= "Physical" and dmgType ~= "Chaos" then
local localInc = calcLocal(modList, "Local"..dmgType.."Damage", "INC", 0) + LocalIncEle
min = round(min * (1 + localInc / 100))
max = round(max * (1 + localInc / 100))
end
if min > 0 and max > 0 then
weaponData[dmgType.."Min"] = min
weaponData[dmgType.."Max"] = max
local dps = (min + max) / 2 * weaponData.AttackRate
weaponData[dmgType.."DPS"] = dps
if dmgType ~= "Physical" and dmgType ~= "Chaos" then
weaponData.ElementalDPS = (weaponData.ElementalDPS or 0) + dps
end
end
end
weaponData.CritChance = round((self.base.weapon.CritChanceBase + calcLocal(modList, "CritChance", "BASE", 0)) * (1 + (calcLocal(modList, "CritChance", "INC", 0) + m_floor(self.quality / 4 * calcLocal(modList, "AlternateQualityLocalCritChancePer4Quality", "INC", 0))) / 100), 2)
for _, value in ipairs(modList:List(nil, "WeaponData")) do
weaponData[value.key] = value.value
end
for _, mod in ipairs(modList) do
-- Convert accuracy, L/MGoH and PAD Leech modifiers to local
if (
(mod.name == "Accuracy" and mod.flags == 0) or (mod.name == "ImpaleChance" and mod.flags ~= ModFlag.Spell) or
((mod.name == "LifeOnHit" or mod.name == "ManaOnHit") and mod.flags == ModFlag.Attack) or
((mod.name == "PhysicalDamageLifeLeech" or mod.name == "PhysicalDamageManaLeech") and mod.flags == ModFlag.Attack)
) and (mod.keywordFlags == 0 or mod.keywordFlags == KeywordFlag.Attack) and not mod[1] then
mod[1] = { type = "Condition", var = (slotNum == 1) and "MainHandAttack" or "OffHandAttack" }
elseif (mod.name == "PoisonChance" or mod.name == "BleedChance") and mod.flags ~= ModFlag.Spell and (not mod[1] or (mod[1].type == "Condition" and mod[1].var == "CriticalStrike" and not mod[2])) then
t_insert(mod, { type = "Condition", var = (slotNum == 1) and "MainHandAttack" or "OffHandAttack" })
end
end
weaponData.TotalDPS = 0
for _, dmgType in pairs(dmgTypeList) do
weaponData.TotalDPS = weaponData.TotalDPS + (weaponData[dmgType.."DPS"] or 0)
end
elseif self.base.armour then
local armourData = self.armourData
local armourBase = calcLocal(modList, "Armour", "BASE", 0) + (self.base.armour.ArmourBaseMin or 0)
local armourVariance = (self.base.armour.ArmourBaseMax or 0) - (self.base.armour.ArmourBaseMin or 0)
local armourEvasionBase = calcLocal(modList, "ArmourAndEvasion", "BASE", 0)
local evasionBase = calcLocal(modList, "Evasion", "BASE", 0) + (self.base.armour.EvasionBaseMin or 0)
local evasionVariance = (self.base.armour.EvasionBaseMax or 0) - (self.base.armour.EvasionBaseMin or 0)
local evasionEnergyShieldBase = calcLocal(modList, "EvasionAndEnergyShield", "BASE", 0)
local energyShieldBase = calcLocal(modList, "EnergyShield", "BASE", 0) + (self.base.armour.EnergyShieldBaseMin or 0)
local energyShieldVariance = (self.base.armour.EnergyShieldBaseMax or 0) - (self.base.armour.EnergyShieldBaseMin or 0)
local armourEnergyShieldBase = calcLocal(modList, "ArmourAndEnergyShield", "BASE", 0)
local wardBase = calcLocal(modList, "Ward", "BASE", 0) + (self.base.armour.WardBaseMin or 0)
local wardVariance = (self.base.armour.WardBaseMax or 0) - (self.base.armour.WardBaseMin or 0)
local armourInc = calcLocal(modList, "Armour", "INC", 0)
local armourEvasionInc = calcLocal(modList, "ArmourAndEvasion", "INC", 0)
local evasionInc = calcLocal(modList, "Evasion", "INC", 0)
local evasionEnergyShieldInc = calcLocal(modList, "EvasionAndEnergyShield", "INC", 0)
local energyShieldInc = calcLocal(modList, "EnergyShield", "INC", 0)
local wardInc = calcLocal(modList, "Ward", "INC", 0)
local armourEnergyShieldInc = calcLocal(modList, "ArmourAndEnergyShield", "INC", 0)
local defencesInc = calcLocal(modList, "Defences", "INC", 0)
local qualityScalar = self.quality
if calcLocal(modList, "AlternateQualityArmour", "BASE", 0) > 0 then
qualityScalar = 0
end
-- base percentiles need to differ for each armour type, as they're weighted differently
if armourData.Armour and armourData.Armour > 0 and not armourData.ArmourBasePercentile then
armourData.ArmourBasePercentile = ((armourData.Armour / ((1 + (armourInc + armourEvasionInc + armourEnergyShieldInc + defencesInc) / 100) * (1 + (qualityScalar / 100))) - armourBase)) / armourVariance
armourData.ArmourBasePercentile = round(m_max(m_min(armourData.ArmourBasePercentile, 1), 0), 4)
end
if armourData.Evasion and armourData.Evasion > 0 and not armourData.EvasionBasePercentile then
armourData.EvasionBasePercentile = ((armourData.Evasion / ((1 + (evasionInc + armourEvasionInc + evasionEnergyShieldInc + defencesInc) / 100) * (1 + (qualityScalar / 100))) - evasionBase)) / evasionVariance
armourData.EvasionBasePercentile = round(m_max(m_min(armourData.EvasionBasePercentile, 1), 0), 4)
end
if armourData.EnergyShield and armourData.EnergyShield > 0 and not armourData.EnergyShieldBasePercentile then
armourData.EnergyShieldBasePercentile = ((armourData.EnergyShield / ((1 + (energyShieldInc + armourEnergyShieldInc + evasionEnergyShieldInc + defencesInc) / 100) * (1 + (qualityScalar / 100))) - energyShieldBase)) / energyShieldVariance
armourData.EnergyShieldBasePercentile = round(m_max(m_min(armourData.EnergyShieldBasePercentile, 1), 0), 4)
end
if armourData.Ward and armourData.Ward > 0 and not armourData.WardBasePercentile then
armourData.WardBasePercentile = ((armourData.Ward / ((1 + (wardInc + defencesInc) / 100) * (1 + (qualityScalar / 100))) - wardBase)) / wardVariance
armourData.WardBasePercentile = round(m_max(m_min(armourData.WardBasePercentile, 1), 0),4)
end
armourData.Armour = round((armourBase + armourEvasionBase + armourEnergyShieldBase + armourVariance * (armourData.ArmourBasePercentile or 1)) * (1 + (armourInc + armourEvasionInc + armourEnergyShieldInc + defencesInc) / 100) * (1 + (qualityScalar / 100)))
armourData.Evasion = round((evasionBase + armourEvasionBase + evasionEnergyShieldBase + evasionVariance * (armourData.EvasionBasePercentile or 1)) * (1 + (evasionInc + armourEvasionInc + evasionEnergyShieldInc + defencesInc) / 100) * (1 + (qualityScalar / 100)))
armourData.EnergyShield = round((energyShieldBase + evasionEnergyShieldBase + armourEnergyShieldBase + energyShieldVariance * (armourData.EnergyShieldBasePercentile or 1)) * (1 + (energyShieldInc + armourEnergyShieldInc + evasionEnergyShieldInc + defencesInc) / 100) * (1 + (qualityScalar / 100)))
armourData.Ward = round((wardBase + wardVariance * (armourData.WardBasePercentile or 1)) * (1 + (wardInc + defencesInc) / 100) * (1 + (qualityScalar / 100)))
if not armourData.ArmourBasePercentile and armourData.Armour > 0 then
armourData.ArmourBasePercentile = 1
end
if not armourData.EvasionBasePercentile and armourData.Evasion > 0 then
armourData.EvasionBasePercentile = 1
end
if not armourData.EnergyShieldBasePercentile and armourData.EnergyShield > 0 then
armourData.EnergyShieldBasePercentile = 1
end
if not armourData.WardBasePercentile and armourData.Ward > 0 then
armourData.WardBasePercentile = 1
end
if self.base.armour.BlockChance then
armourData.BlockChance = m_floor((self.base.armour.BlockChance + calcLocal(modList, "BlockChance", "BASE", 0)) * (1 + calcLocal(modList, "BlockChance", "INC", 0) / 100))
end
if self.base.armour.MovementPenalty then
modList:NewMod("MovementSpeed", "INC", -self.base.armour.MovementPenalty, self.modSource, { type = "Condition", var = "IgnoreMovementPenalties", neg = true })
end
for _, value in ipairs(modList:List(nil, "ArmourData")) do
armourData[value.key] = value.value
end
elseif self.base.flask then
local flaskData = self.flaskData
local durationInc = calcLocal(modList, "Duration", "INC", 0)
local durationMore = calcLocal(modList, "Duration", "MORE", 0)
if self.base.flask.life or self.base.flask.mana then
-- Recovery flask
flaskData.instantPerc = calcLocal(modList, "FlaskInstantRecovery", "BASE", 0)
local recoveryMod = 1 + calcLocal(modList, "FlaskRecovery", "INC", 0) / 100
local rateMod = 1 + calcLocal(modList, "FlaskRecoveryRate", "INC", 0) / 100
flaskData.duration = round(self.base.flask.duration * (1 + durationInc / 100) / rateMod * durationMore, 1)
if self.base.flask.life then
flaskData.lifeBase = self.base.flask.life * (1 + self.quality / 100) * recoveryMod
flaskData.lifeInstant = flaskData.lifeBase * flaskData.instantPerc / 100
flaskData.lifeGradual = flaskData.lifeBase * (1 - flaskData.instantPerc / 100)
flaskData.lifeTotal = flaskData.lifeInstant + flaskData.lifeGradual
flaskData.lifeAdditional = calcLocal(modList, "FlaskAdditionalLifeRecovery", "BASE", 0)
flaskData.lifeEffectNotRemoved = calcLocal(baseList, "LifeFlaskEffectNotRemoved", "FLAG", 0)
end
if self.base.flask.mana then
flaskData.manaBase = self.base.flask.mana * (1 + self.quality / 100) * recoveryMod
flaskData.manaInstant = flaskData.manaBase * flaskData.instantPerc / 100
flaskData.manaGradual = flaskData.manaBase * (1 - flaskData.instantPerc / 100)
flaskData.manaTotal = flaskData.manaInstant + flaskData.manaGradual
flaskData.manaEffectNotRemoved = calcLocal(baseList, "ManaFlaskEffectNotRemoved", "FLAG", 0)
end
else
-- Utility flask
flaskData.duration = round(self.base.flask.duration * (1 + durationInc / 100) * (1 + self.quality / 100) * durationMore, 1)
end
flaskData.chargesMax = self.base.flask.chargesMax + calcLocal(modList, "FlaskCharges", "BASE", 0)
flaskData.chargesUsed = m_floor(self.base.flask.chargesUsed * (1 + calcLocal(modList, "FlaskChargesUsed", "INC", 0) / 100))
flaskData.gainMod = 1 + calcLocal(modList, "FlaskChargeRecovery", "INC", 0) / 100
flaskData.effectInc = calcLocal(modList, "FlaskEffect", "INC", 0) + calcLocal(modList, "LocalEffect", "INC", 0)
for _, value in ipairs(modList:List(nil, "FlaskData")) do
flaskData[value.key] = value.value
end
elseif self.base.tincture then
local tinctureData = self.tinctureData
tinctureData.manaBurn = (self.base.tincture.manaBurn + 0.01) / (1 + calcLocal(modList, "TinctureManaBurnRate", "INC", 0) / 100) / (1 + calcLocal(modList, "TinctureManaBurnRate", "MORE", 0) / 100)
tinctureData.cooldown = self.base.tincture.cooldown / (1 + calcLocal(modList, "TinctureCooldownRecovery", "INC", 0) / 100)
tinctureData.effectInc = calcLocal(modList, "TinctureEffect", "INC", 0) + calcLocal(modList, "LocalEffect", "INC", 0)
for _, value in ipairs(modList:List(nil, "TinctureData")) do
tinctureData[value.key] = value.value
end
elseif self.type == "Jewel" then
if self.name:find("Grand Spectrum") then
local spectrumMod = modLib.createMod("Multiplier:GrandSpectrum", "BASE", 1, self.name)
modList:AddMod(spectrumMod)
modList:NewMod("MinionModifier", "LIST", { mod = spectrumMod }, self.name)
end
local jewelData = self.jewelData
for _, func in ipairs(modList:List(nil, "JewelFunc")) do
jewelData.funcList = jewelData.funcList or { }
t_insert(jewelData.funcList, func)
end
for _, value in ipairs(modList:List(nil, "JewelData")) do
jewelData[value.key] = value.value
end
if modList:List(nil, "ImpossibleEscapeKeystones") then
jewelData.impossibleEscapeKeystones = { }
for _, value in ipairs(modList:List(nil, "ImpossibleEscapeKeystones")) do
jewelData.impossibleEscapeKeystones[value.key] = value.value
end
end
if self.clusterJewel then
jewelData.clusterJewelNotables = { }
for _, name in ipairs(modList:List(nil, "ClusterJewelNotable")) do
t_insert(jewelData.clusterJewelNotables, name)
end
jewelData.clusterJewelAddedMods = { }
for _, line in ipairs(modList:List(nil, "AddToClusterJewelNode")) do
t_insert(jewelData.clusterJewelAddedMods, line)
end
-- Small and Medium Curse Cluster Jewel passive mods are parsed the same so the medium cluster data overwrites small and the skills differ
-- This changes small curse clusters to have the correct clusterJewelSkill so it passes validation below and works as expected in the tree
if jewelData.clusterJewelSkill == "affliction_curse_effect" and jewelData.clusterJewelNodeCount and jewelData.clusterJewelNodeCount < 4 then
jewelData.clusterJewelSkill = "affliction_curse_effect_small"
end
-- Validation
if jewelData.clusterJewelNodeCount then
jewelData.clusterJewelNodeCount = m_min(m_max(jewelData.clusterJewelNodeCount, self.clusterJewel.minNodes), self.clusterJewel.maxNodes)
end
if jewelData.clusterJewelSkill and not self.clusterJewel.skills[jewelData.clusterJewelSkill] then
jewelData.clusterJewelSkill = nil
end
jewelData.clusterJewelValid = jewelData.clusterJewelKeystone
or ((jewelData.clusterJewelSkill or jewelData.clusterJewelSmallsAreNothingness) and jewelData.clusterJewelNodeCount)
or (jewelData.clusterJewelSocketCountOverride and jewelData.clusterJewelNothingnessCount)
end
end
return { unpack(modList) }
end
-- Build lists of modifiers for each slot the item can occupy
function ItemClass:BuildModList()
if not self.base then
return
end
local baseList = new("ModList")
if self.base.weapon then
self.weaponData = { }
elseif self.base.armour then
self.armourData = self.armourData or { }
elseif self.base.flask then
self.flaskData = { }
self.buffModList = { }
elseif self.base.tincture then
self.tinctureData = { }
self.buffModList = { }
elseif self.type == "Jewel" then
self.jewelData = { }
end
self.baseModList = baseList
self.rangeLineList = { }
self.modSource = "Item:"..(self.id or -1)..":"..self.name
for _, modLine in ipairs(self.buffModLines) do
if not modLine.extra and self:CheckModLineVariant(modLine) then
for _, mod in ipairs(modLine.modList) do
mod.source = self.modSource
t_insert(self.buffModList, mod)
end
end
end
local function processModLine(modLine)
if self:CheckModLineVariant(modLine) then
-- special section for variant over-ride of pre-modifier item parameters
if modLine.line:find("Requires Class") then
self.classRestriction = modLine.line:gsub("{variant:([%d,]+)}", ""):match("Requires Class (.+)")
end
-- handle understood modifier variable properties
if not modLine.extra then
if modLine.range then
-- Check if line actually has a range
if modLine.line:find("%((%-?%d+%.?%d*)%-(%-?%d+%.?%d*)%)") then
local strippedModeLine = modLine.line:gsub("\n"," ")
local catalystScalar = getCatalystScalar(self.catalyst, modLine.modTags, self.catalystQuality)
-- Put the modified value into the string
local line = itemLib.applyRange(strippedModeLine, modLine.range, catalystScalar)
-- Check if we can parse it before adding the mods
local list, extra = modLib.parseMod(line)
if list and not extra then
modLine.modList = list
t_insert(self.rangeLineList, modLine)
end
end
end
for _, mod in ipairs(modLine.modList) do
mod = modLib.setSource(mod, self.modSource)
baseList:AddMod(mod)
end
if modLine.modTags and #modLine.modTags > 0 then
self.hasModTags = true
end
end
end
end
for _, modLine in ipairs(self.enchantModLines) do
processModLine(modLine)
end
for _, modLine in ipairs(self.scourgeModLines) do
processModLine(modLine)
end
for _, modLine in ipairs(self.classRequirementModLines) do
processModLine(modLine)
end
for _, modLine in ipairs(self.implicitModLines) do
processModLine(modLine)
end
for _, modLine in ipairs(self.explicitModLines) do
processModLine(modLine)
end
for _, modLine in ipairs(self.crucibleModLines) do
processModLine(modLine)
end
if self.name == "Tabula Rasa, Simple Robe" or self.name == "Skin of the Loyal, Simple Robe" or self.name == "Skin of the Lords, Simple Robe" or self.name == "The Apostate, Cabalist Regalia" then
-- Hack to remove the energy shield and base int requirement
baseList:NewMod("ArmourData", "LIST", { key = "EnergyShield", value = 0 })
self.requirements.int = 0
end
if calcLocal(baseList, "NoAttributeRequirements", "FLAG", 0) then
self.requirements.strMod = 0
self.requirements.dexMod = 0
self.requirements.intMod = 0
else
self.requirements.strMod = m_floor((self.requirements.str + calcLocal(baseList, "StrRequirement", "BASE", 0)) * (1 + calcLocal(baseList, "StrRequirement", "INC", 0) / 100))
self.requirements.dexMod = m_floor((self.requirements.dex + calcLocal(baseList, "DexRequirement", "BASE", 0)) * (1 + calcLocal(baseList, "DexRequirement", "INC", 0) / 100))
self.requirements.intMod = m_floor((self.requirements.int + calcLocal(baseList, "IntRequirement", "BASE", 0)) * (1 + calcLocal(baseList, "IntRequirement", "INC", 0) / 100))
end
self.grantedSkills = { }
for _, skill in ipairs(baseList:List(nil, "ExtraSkill")) do
if skill.name ~= "Unknown" then
t_insert(self.grantedSkills, {
skillId = skill.skillId,
level = skill.level,
noSupports = skill.noSupports,
source = self.modSource,
triggered = skill.triggered,
triggerChance = skill.triggerChance,
})
end
end
local socketCount = calcLocal(baseList, "SocketCount", "BASE", 0)
self.abyssalSocketCount = calcLocal(baseList, "AbyssalSocketCount", "BASE", 0)
self.selectableSocketCount = m_max(self.base.socketLimit or 0, #self.sockets) - self.abyssalSocketCount
if calcLocal(baseList, "NoSockets", "FLAG", 0) then
-- Remove all sockets
wipeTable(self.sockets)
self.selectableSocketCount = 0
elseif socketCount > 0 then
-- Force the socket count to be equal to the stated number
self.selectableSocketCount = socketCount
local group = 0
for i = 1, m_max(socketCount, #self.sockets) do
if i > socketCount then
self.sockets[i] = nil
elseif not self.sockets[i] then
self.sockets[i] = {
color = self.defaultSocketColor,
group = group
}
else
group = self.sockets[i].group
end
end
elseif self.abyssalSocketCount > 0 then
-- Ensure that there are the correct number of abyssal sockets present
local newSockets = { }
local group = 0
if self.sockets then
for i, socket in ipairs(self.sockets) do
if socket.color ~= "A" then
if #newSockets >= self.selectableSocketCount then
break
end
t_insert(newSockets, socket)
group = socket.group
end
end
end
for i = 1, self.abyssalSocketCount do
group = group + 1
t_insert(newSockets, {
color = "A",
group = group
})
end
self.sockets = newSockets
end
self.socketedJewelEffectModifier = 1 + calcLocal(baseList, "SocketedJewelEffect", "INC", 0) / 100
if self.base.weapon or self.type == "Ring" then
self.slotModList = { }
for i = 1, 2 do
self.slotModList[i] = self:BuildModListForSlotNum(baseList, i)
end
else
self.modList = self:BuildModListForSlotNum(baseList)
end
end