Adds pantheons from temmings' pull request Adds partial Impale support from baranio's pull request Adds updated uniques from PJacek's pull request and my own Adds more tree highlighting options for node power from coldino's pull request Adds support for fossil mods in the crafting window. Including correct parsing for some mods that previously didn't work
919 lines
32 KiB
Lua
919 lines
32 KiB
Lua
-- Path of Building
|
|
--
|
|
-- Class: Item
|
|
-- Equippable item class
|
|
--
|
|
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 ItemClass = newClass("Item", function(self, targetVersion, raw)
|
|
self.targetVersion = targetVersion
|
|
if raw then
|
|
self:ParseRaw(itemLib.sanitiseItemText(raw))
|
|
end
|
|
end)
|
|
|
|
-- Parse raw item data and extract item name, base type, quality, and modifiers
|
|
function ItemClass:ParseRaw(raw)
|
|
self.raw = raw
|
|
local verData = data[self.targetVersion]
|
|
self.name = "?"
|
|
self.rarity = "UNIQUE"
|
|
self.quality = nil
|
|
self.rawLines = { }
|
|
for line in string.gmatch(self.raw .. "\r\n", "([^\r\n]*)\r?\n") do
|
|
line = line:gsub("^%s+",""):gsub("%s+$","")
|
|
if #line > 0 then
|
|
t_insert(self.rawLines, line)
|
|
end
|
|
end
|
|
local mode = "WIKI"
|
|
local l = 1
|
|
if self.rawLines[l] then
|
|
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 == "NORMAL" then
|
|
-- Hack for relics
|
|
for _, line in ipairs(self.rawLines) do
|
|
if line == "Relic Unique" then
|
|
self.rarity = "RELIC"
|
|
break
|
|
end
|
|
end
|
|
end
|
|
l = l + 1
|
|
end
|
|
end
|
|
if self.rawLines[l] then
|
|
self.name = self.rawLines[l]
|
|
l = l + 1
|
|
end
|
|
self.namePrefix = ""
|
|
self.nameSuffix = ""
|
|
if self.rarity == "NORMAL" or self.rarity == "MAGIC" then
|
|
for baseName, baseData in pairs(verData.itemBases) do
|
|
local s, e = self.name:find(baseName, 1, true)
|
|
if s then
|
|
self.baseName = baseName
|
|
self.namePrefix = self.name:sub(1, s - 1)
|
|
self.nameSuffix = self.name:sub(e + 1)
|
|
self.type = baseData.type
|
|
break
|
|
end
|
|
end
|
|
if not self.baseName then
|
|
local s, e = self.name:find("Two-Toned Boots", 1, true)
|
|
if s then
|
|
-- Hack for Two-Toned Boots
|
|
self.baseName = "Two-Toned Boots (Armour/Energy Shield)"
|
|
self.namePrefix = self.name:sub(1, s - 1)
|
|
self.nameSuffix = self.name:sub(e + 1)
|
|
self.type = "Boots"
|
|
end
|
|
end
|
|
self.name = self.name:gsub(" %(.+%)","")
|
|
elseif self.rawLines[l] and not self.rawLines[l]:match("^%-") then
|
|
if self.rawLines[l] == "Two-Toned Boots" then
|
|
self.rawLines[l] = "Two-Toned Boots (Armour/Energy Shield)"
|
|
end
|
|
local baseName = self.rawLines[l]:gsub("Synthesised ","")
|
|
if verData.itemBases[baseName] then
|
|
self.baseName = baseName
|
|
self.title = self.name
|
|
self.name = self.title .. ", " .. baseName:gsub(" %(.+%)","")
|
|
self.type = verData.itemBases[baseName].type
|
|
l = l + 1
|
|
end
|
|
end
|
|
self.base = verData.itemBases[self.baseName]
|
|
self.sockets = { }
|
|
self.modLines = { }
|
|
self.implicitLines = 0
|
|
self.buffLines = 0
|
|
if self.base then
|
|
self.affixes = (self.base.subType and verData.itemMods[self.base.type..self.base.subType])
|
|
or verData.itemMods[self.base.type]
|
|
or verData.itemMods.Item
|
|
self.enchantments = verData.enchantments[self.base.type]
|
|
self.corruptable = self.base.type ~= "Flask"
|
|
self.shaperElderTags = data.specialBaseTags[self.type]
|
|
self.canBeShaperElder = self.shaperElderTags
|
|
end
|
|
self.variantList = nil
|
|
self.prefixes = { }
|
|
self.suffixes = { }
|
|
self.requirements = { }
|
|
if self.base then
|
|
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"
|
|
end
|
|
local importedLevelReq
|
|
local flaskBuffLines = { }
|
|
if self.base and self.base.flask and self.base.flask.buff then
|
|
self.buffLines = #self.base.flask.buff
|
|
for _, line in ipairs(self.base.flask.buff) do
|
|
flaskBuffLines[line] = true
|
|
local modList, extra = modLib.parseMod[self.targetVersion](line)
|
|
t_insert(self.modLines, { line = line, extra = extra, modList = modList or { }, buff = true })
|
|
end
|
|
end
|
|
local gameModeStage = "FINDIMPLICIT"
|
|
local gameModeSection = 1
|
|
local foundExplicit
|
|
while self.rawLines[l] do
|
|
local line = self.rawLines[l]
|
|
if flaskBuffLines[line] then
|
|
flaskBuffLines[line] = nil
|
|
elseif line == "--------" then
|
|
gameModeSection = gameModeSection + 1
|
|
if gameModeStage == "IMPLICIT" then
|
|
self.implicitLines = #self.modLines - self.buffLines
|
|
gameModeStage = "FINDEXPLICIT"
|
|
elseif gameModeStage == "EXPLICIT" then
|
|
gameModeStage = "DONE"
|
|
end
|
|
elseif line == "Corrupted" then
|
|
self.corrupted = true
|
|
elseif line == "Shaper Item" then
|
|
self.shaper = true
|
|
elseif line == "Elder Item" then
|
|
self.elder = true
|
|
elseif line == "Fractured Item" then
|
|
self.fractured = true
|
|
elseif line == "Synthesised Item" then
|
|
self.synthesised = true
|
|
else
|
|
local specName, specVal = line:match("^([%a ]+): (%x+)$")
|
|
if not specName then
|
|
specName, specVal = line:match("^([%a ]+): %+?([%d%-%.]+)")
|
|
if not tonumber(specVal) then
|
|
specName = nil
|
|
end
|
|
end
|
|
if not specName then
|
|
specName, specVal = line:match("^([%a ]+): (.+)$")
|
|
end
|
|
if not specName then
|
|
specName, specVal = line:match("^(Requires) (.+)$")
|
|
end
|
|
if specName then
|
|
if specName == "Unique ID" then
|
|
self.uniqueID = specVal
|
|
elseif specName == "Item Level" then
|
|
self.itemLevel = tonumber(specVal)
|
|
elseif specName == "Quality" then
|
|
self.quality = tonumber(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
|
|
for index, data in pairs(verData.jewelRadius) do
|
|
if specVal:match("^%a+") == data.label then
|
|
self.jewelRadiusIndex = index
|
|
break
|
|
end
|
|
end
|
|
elseif specName == "Limited to" and self.type == "Jewel" then
|
|
self.limit = tonumber(specVal)
|
|
elseif specName == "Variant" then
|
|
if not self.variantList then
|
|
self.variantList = { }
|
|
end
|
|
local ver, name = specVal:match("{([%w_]+)}(.+)")
|
|
if ver then
|
|
t_insert(self.variantList, name)
|
|
if ver == self.targetVersion then
|
|
self.defaultVariant = #self.variantList
|
|
end
|
|
else
|
|
t_insert(self.variantList, specVal)
|
|
end
|
|
elseif specName == "Requires" then
|
|
self.requirements.level = tonumber(specVal:match("Level (%d+)"))
|
|
elseif specName == "Level" then
|
|
-- Requirements from imported items can't always be trusted
|
|
importedLevelReq = tonumber(specVal)
|
|
elseif specName == "LevelReq" then
|
|
self.requirements.level = tonumber(specVal)
|
|
elseif specName == "Has Alt Variant" then
|
|
self.hasAltVariant = true
|
|
elseif specName == "Selected Variant" then
|
|
self.variant = tonumber(specVal)
|
|
elseif specName == "Selected Alt Variant" then
|
|
self.variantAlt = tonumber(specVal)
|
|
elseif specName == "League" then
|
|
self.league = specVal
|
|
elseif specName == "Crafted" then
|
|
self.crafted = true
|
|
elseif specName == "Prefix" then
|
|
local range, affix = specVal:match("{range:([%d.]+)}(.+)")
|
|
t_insert(self.prefixes, {
|
|
modId = affix or specVal,
|
|
range = tonumber(range),
|
|
})
|
|
elseif specName == "Suffix" then
|
|
local range, affix = specVal:match("{range:([%d.]+)}(.+)")
|
|
t_insert(self.suffixes, {
|
|
modId = affix or specVal,
|
|
range = tonumber(range),
|
|
})
|
|
elseif specName == "Implicits" then
|
|
self.implicitLines = tonumber(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 == "Evasion Rating" then
|
|
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 = verData.itemBases[self.baseName]
|
|
end
|
|
elseif specName == "Energy Shield" then
|
|
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 = verData.itemBases[self.baseName]
|
|
end
|
|
end
|
|
end
|
|
if line == "Prefixes:" then
|
|
foundExplicit = true
|
|
gameModeStage = "EXPLICIT"
|
|
end
|
|
if not specName or foundExplicit then
|
|
local varSpec = line:match("{variant:([%d,]+)}")
|
|
local variantList
|
|
if varSpec then
|
|
variantList = { }
|
|
for varId in varSpec:gmatch("%d+") do
|
|
variantList[tonumber(varId)] = true
|
|
end
|
|
end
|
|
local fractured = line:match("{fractured}") or line:match(" %(fractured%)")
|
|
local rangeSpec = line:match("{range:([%d.]+)}")
|
|
local crafted = line:match("{crafted}") or line:match(" %(crafted%)")
|
|
local custom = line:match("{custom}")
|
|
line = line:gsub("%b{}", ""):gsub(" %(fractured%)",""):gsub(" %(crafted%)","")
|
|
local rangedLine
|
|
if line:match("%(%d+%-%d+ to %d+%-%d+%)") or line:match("%(%-?[%d%.]+ to %-?[%d%.]+%)") or line:match("%(%-?[%d%.]+%-[%d%.]+%)") then
|
|
rangedLine = itemLib.applyRange(line, 1)
|
|
end
|
|
local modList, extra = modLib.parseMod[self.targetVersion](rangedLine or line)
|
|
if (not modList or extra) and self.rawLines[l+1] then
|
|
-- Try to combine it with the next line
|
|
local combLine = line.." "..self.rawLines[l+1]
|
|
if combLine:match("%(%d+%-%d+ to %d+%-%d+%)") or combLine:match("%(%-?[%d%.]+ to %-?[%d%.]+%)") or combLine:match("%(%-?[%d%.]+%-[%d%.]+%)") then
|
|
rangedLine = itemLib.applyRange(combLine, 1)
|
|
end
|
|
modList, extra = modLib.parseMod[self.targetVersion](rangedLine or combLine, true)
|
|
if modList and not extra then
|
|
line = line.."\n"..self.rawLines[l+1]
|
|
l = l + 1
|
|
else
|
|
modList, extra = modLib.parseMod[self.targetVersion](rangedLine or line)
|
|
end
|
|
end
|
|
if modList then
|
|
t_insert(self.modLines, { line = line, extra = extra, modList = modList, variantList = variantList, crafted = crafted, custom = custom, fractured = fractured, range = rangedLine and (tonumber(rangeSpec) or 0.5) })
|
|
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" then
|
|
t_insert(self.modLines, { line = line, extra = line, modList = { }, variantList = variantList, crafted = crafted, custom = custom, fractured = fractured })
|
|
elseif gameModeStage == "FINDEXPLICIT" then
|
|
gameModeStage = "DONE"
|
|
end
|
|
elseif foundExplicit then
|
|
t_insert(self.modLines, { line = line, extra = line, modList = { }, variantList = variantList, crafted = crafted, custom = custom, fractured = fractured })
|
|
end
|
|
end
|
|
end
|
|
l = l + 1
|
|
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
|
|
if self.base and self.base.implicit then
|
|
if self.implicitLines == 0 then
|
|
self.implicitLines = 1 + #self.base.implicit:gsub("[^\n]","")
|
|
end
|
|
elseif mode == "GAME" and not foundExplicit then
|
|
self.implicitLines = 0
|
|
end
|
|
self.affixLimit = 0
|
|
if self.crafted then
|
|
if not self.affixes then
|
|
self.crafted = false
|
|
elseif self.rarity == "MAGIC" then
|
|
self.affixLimit = 2
|
|
elseif self.rarity == "RARE" then
|
|
self.affixLimit = (self.type == "Jewel" and 4 or 6)
|
|
else
|
|
self.crafted = false
|
|
end
|
|
if self.crafted then
|
|
for _, list in ipairs({self.prefixes,self.suffixes}) do
|
|
for i = 1, 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.defaultVariant or #self.variantList)
|
|
if self.hasAltVariant then
|
|
self.variantAlt = m_min(#self.variantList, self.variantAlt or self.defaultVariant or #self.variantList)
|
|
end
|
|
end
|
|
if not self.quality then
|
|
self:NormaliseQuality()
|
|
end
|
|
self:BuildModList()
|
|
end
|
|
|
|
function ItemClass:NormaliseQuality()
|
|
if self.base and (self.base.armour or self.base.weapon or self.base.flask) then
|
|
if not self.quality then
|
|
self.quality = self.corrupted and 0 or 20
|
|
elseif not self.uniqueID and not self.corrupted then
|
|
self.quality = 20
|
|
end
|
|
end
|
|
end
|
|
|
|
function ItemClass:GetModSpawnWeight(mod, extraTags)
|
|
if self.base then
|
|
for i, key in ipairs(mod.weightKey) do
|
|
if self.base.tags[key] or (extraTags and extraTags[key]) or (self.shaperElderTags and (self.shaper and self.shaperElderTags.shaper == key) or (self.elder and self.shaperElderTags.elder == key)) then
|
|
return mod.weightVal[i]
|
|
end
|
|
end
|
|
end
|
|
return 0
|
|
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.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
|
|
if self.shaper then
|
|
t_insert(rawLines, "Shaper Item")
|
|
end
|
|
if self.elder then
|
|
t_insert(rawLines, "Elder Item")
|
|
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.itemLevel then
|
|
t_insert(rawLines, "Item Level: "..self.itemLevel)
|
|
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)
|
|
if self.hasAltVariant then
|
|
t_insert(rawLines, "Has Alt Variant: true")
|
|
t_insert(rawLines, "Selected Alt Variant: "..self.variantAlt)
|
|
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.jewelRadiusIndex then
|
|
t_insert(rawLines, "Radius: "..data.jewelRadius[self.jewelRadiusIndex].label)
|
|
end
|
|
if self.limit then
|
|
t_insert(rawLines, "Limited to: "..self.limit)
|
|
end
|
|
t_insert(rawLines, "Implicits: "..self.implicitLines)
|
|
for _, modLine in ipairs(self.modLines) do
|
|
if not modLine.buff then
|
|
local line = modLine.line
|
|
if modLine.range 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.fractured then
|
|
line = "{fractured}" .. 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
|
|
t_insert(rawLines, line)
|
|
end
|
|
end
|
|
if self.corrupted 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()
|
|
local custom = { }
|
|
for l = self.buffLines + self.implicitLines + 1, #self.modLines do
|
|
local modLine = self.modLines[l]
|
|
if modLine.custom or modLine.crafted then
|
|
t_insert(custom, modLine)
|
|
end
|
|
self.modLines[l] = nil
|
|
end
|
|
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, 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 .. " "
|
|
elseif mod.type == "Suffix" then
|
|
self.nameSuffix = " " .. mod.affix
|
|
end
|
|
self.requirements.level = m_max(self.requirements.level or 0, m_floor(mod.level * 0.8))
|
|
for i, line in ipairs(mod) do
|
|
line = itemLib.applyRange(line, affix.range or 0.5)
|
|
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 = self.buffLines + self.implicitLines + 1, #self.modLines + 1 do
|
|
if not self.modLines[l] or self.modLines[l].order > order then
|
|
t_insert(self.modLines, l, modLine)
|
|
break
|
|
end
|
|
end
|
|
statOrder[order] = modLine
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
for _, line in ipairs(custom) do
|
|
t_insert(self.modLines, line)
|
|
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])
|
|
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
|
|
|
|
-- Add up 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 sumLocal(modList, name, type, flags)
|
|
local result
|
|
if type == "FLAG" then
|
|
result = false
|
|
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
|
|
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 modifers 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
|
|
if self.base.weapon then
|
|
local weaponData = { }
|
|
self.weaponData[slotNum] = weaponData
|
|
weaponData.type = self.base.type
|
|
weaponData.name = self.name
|
|
weaponData.AttackSpeedInc = sumLocal(modList, "Speed", "INC", ModFlag.Attack)
|
|
weaponData.AttackRate = round(self.base.weapon.AttackRateBase * (1 + weaponData.AttackSpeedInc / 100), 2)
|
|
weaponData.range = self.base.weapon.Range + sumLocal(modList, "WeaponRange", "BASE", 0)
|
|
for _, dmgType in pairs(dmgTypeList) do
|
|
local min = (self.base.weapon[dmgType.."Min"] or 0) + sumLocal(modList, dmgType.."Min", "BASE", 0)
|
|
local max = (self.base.weapon[dmgType.."Max"] or 0) + sumLocal(modList, dmgType.."Max", "BASE", 0)
|
|
if dmgType == "Physical" then
|
|
local physInc = sumLocal(modList, "PhysicalDamage", "INC", 0)
|
|
min = round(min * (1 + (physInc + self.quality) / 100))
|
|
max = round(max * (1 + (physInc + self.quality) / 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 * (1 + sumLocal(modList, "CritChance", "INC", 0) / 100), 2)
|
|
for _, value in ipairs(modList:List(nil, "WeaponData")) do
|
|
weaponData[value.key] = value.value
|
|
end
|
|
if self.targetVersion == "2_6" then
|
|
local accuracyInc = sumLocal(modList, "Accuracy", "INC", 0)
|
|
if accuracyInc > 0 then
|
|
modList:NewMod("Accuracy", "MORE", accuracyInc, self.modSource, { type = "Condition", var = (slotNum == 1) and "MainHandAttack" or "OffHandAttack" })
|
|
end
|
|
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" ) 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 self.targetVersion ~= "2_6" and (mod.name == "PoisonChance" or mod.name == "BleedChance") 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 = sumLocal(modList, "Armour", "BASE", 0) + (self.base.armour.ArmourBase or 0)
|
|
local armourEvasionBase = sumLocal(modList, "ArmourAndEvasion", "BASE", 0)
|
|
local evasionBase = sumLocal(modList, "Evasion", "BASE", 0) + (self.base.armour.EvasionBase or 0)
|
|
local evasionEnergyShieldBase = sumLocal(modList, "EvasionAndEnergyShield", "BASE", 0)
|
|
local energyShieldBase = sumLocal(modList, "EnergyShield", "BASE", 0) + (self.base.armour.EnergyShieldBase or 0)
|
|
local armourEnergyShieldBase = sumLocal(modList, "ArmourAndEnergyShield", "BASE", 0)
|
|
local armourInc = sumLocal(modList, "Armour", "INC", 0)
|
|
local armourEvasionInc = sumLocal(modList, "ArmourAndEvasion", "INC", 0)
|
|
local evasionInc = sumLocal(modList, "Evasion", "INC", 0)
|
|
local evasionEnergyShieldInc = sumLocal(modList, "EvasionAndEnergyShield", "INC", 0)
|
|
local energyShieldInc = sumLocal(modList, "EnergyShield", "INC", 0)
|
|
local armourEnergyShieldInc = sumLocal(modList, "ArmourAndEnergyShield", "INC", 0)
|
|
local defencesInc = sumLocal(modList, "Defences", "INC", 0)
|
|
armourData.Armour = round((armourBase + armourEvasionBase + armourEnergyShieldBase) * (1 + (armourInc + armourEvasionInc + armourEnergyShieldInc + defencesInc + self.quality) / 100))
|
|
armourData.Evasion = round((evasionBase + armourEvasionBase + evasionEnergyShieldBase) * (1 + (evasionInc + armourEvasionInc + evasionEnergyShieldInc + defencesInc + self.quality) / 100))
|
|
armourData.EnergyShield = round((energyShieldBase + evasionEnergyShieldBase + armourEnergyShieldBase) * (1 + (energyShieldInc + armourEnergyShieldInc + evasionEnergyShieldInc + defencesInc + self.quality) / 100))
|
|
if self.base.armour.BlockChance then
|
|
armourData.BlockChance = self.base.armour.BlockChance + sumLocal(modList, "BlockChance", "BASE", 0)
|
|
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 = sumLocal(modList, "Duration", "INC", 0)
|
|
if self.base.flask.life or self.base.flask.mana then
|
|
-- Recovery flask
|
|
flaskData.instantPerc = sumLocal(modList, "FlaskInstantRecovery", "BASE", 0)
|
|
local recoveryMod = 1 + sumLocal(modList, "FlaskRecovery", "INC", 0) / 100
|
|
local rateMod = 1 + sumLocal(modList, "FlaskRecoveryRate", "INC", 0) / 100
|
|
flaskData.duration = self.base.flask.duration * (1 + durationInc / 100) / rateMod
|
|
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) * (1 + durationInc / 100)
|
|
flaskData.lifeTotal = flaskData.lifeInstant + flaskData.lifeGradual
|
|
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) * (1 + durationInc / 100)
|
|
flaskData.manaTotal = flaskData.manaInstant + flaskData.manaGradual
|
|
end
|
|
else
|
|
-- Utility flask
|
|
flaskData.duration = self.base.flask.duration * (1 + (durationInc + self.quality) / 100)
|
|
end
|
|
flaskData.chargesMax = self.base.flask.chargesMax + sumLocal(modList, "FlaskCharges", "BASE", 0)
|
|
flaskData.chargesUsed = m_floor(self.base.flask.chargesUsed * (1 + sumLocal(modList, "FlaskChargesUsed", "INC", 0) / 100))
|
|
flaskData.gainMod = 1 + sumLocal(modList, "FlaskChargeRecovery", "INC", 0) / 100
|
|
flaskData.effectInc = sumLocal(modList, "FlaskEffect", "INC", 0)
|
|
for _, value in ipairs(modList:List(nil, "FlaskData")) do
|
|
flaskData[value.key] = value.value
|
|
end
|
|
elseif self.type == "Jewel" then
|
|
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
|
|
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 = { }
|
|
elseif self.base.flask then
|
|
self.flaskData = { }
|
|
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.modLines) do
|
|
if not modLine.extra and self:CheckModLineVariant(modLine) then
|
|
if modLine.range then
|
|
local line = itemLib.applyRange(modLine.line:gsub("\n"," "), modLine.range)
|
|
local list, extra = modLib.parseMod[self.targetVersion](line)
|
|
if list and not extra then
|
|
modLine.modList = list
|
|
t_insert(self.rangeLineList, modLine)
|
|
end
|
|
end
|
|
for _, mod in ipairs(modLine.modList) do
|
|
mod.source = self.modSource
|
|
if type(mod.value) == "table" and mod.value.mod then
|
|
mod.value.mod.source = mod.source
|
|
end
|
|
if modLine.buff then
|
|
t_insert(self.buffModList, mod)
|
|
else
|
|
baseList:AddMod(mod)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if sumLocal(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 + sumLocal(baseList, "StrRequirement", "BASE", 0)) * (1 + sumLocal(baseList, "StrRequirement", "INC", 0) / 100))
|
|
self.requirements.dexMod = m_floor((self.requirements.dex + sumLocal(baseList, "DexRequirement", "BASE", 0)) * (1 + sumLocal(baseList, "DexRequirement", "INC", 0) / 100))
|
|
self.requirements.intMod = m_floor((self.requirements.int + sumLocal(baseList, "IntRequirement", "BASE", 0)) * (1 + sumLocal(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,
|
|
})
|
|
end
|
|
end
|
|
local socketCount = sumLocal(baseList, "SocketCount", "BASE", 0)
|
|
self.abyssalSocketCount = sumLocal(baseList, "AbyssalSocketCount", "BASE", 0)
|
|
self.selectableSocketCount = m_max(self.base.socketLimit or 0, #self.sockets) - self.abyssalSocketCount
|
|
if sumLocal(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
|
|
t_insert(newSockets, socket)
|
|
group = socket.group
|
|
if #newSockets >= self.selectableSocketCount then
|
|
break
|
|
end
|
|
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 + sumLocal(baseList, "SocketedJewelEffect", "INC", 0) / 100
|
|
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" then
|
|
-- Hack to remove the energy shield
|
|
baseList:NewMod("ArmourData", "LIST", { key = "EnergyShield", value = 0 })
|
|
end
|
|
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
|