Files
PathOfBuilding/Modules/ItemTools.lua
Openarl 56db57024a Sneaking in more features
- Added MoM total to Calcs tab
- Quality normalisation now only occurs when an item is first added
2017-02-21 02:48:31 +10:00

614 lines
21 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- Path of Building
--
-- Module: Item Tools
-- Various functions for dealing with items.
--
local t_insert = table.insert
local t_remove = table.remove
local m_min = math.min
local m_floor = math.floor
local dmgTypeList = {"Physical", "Lightning", "Cold", "Fire", "Chaos"}
itemLib = { }
-- Apply range value (0 to 1) to a modifier that has a range: (x to x) or (x-x to x-x)
function itemLib.applyRange(line, range)
return line:gsub("%((%d+)%-(%d+) to (%d+)%-(%d+)%)", "(%1-%2) to (%3-%4)")
:gsub("(%+?)%((%-?%d+) to (%d+)%)", "%1(%2-%3)")
:gsub("(%+?)%((%-?%d+)%-(%d+)%)",
function(plus, min, max)
local numVal = m_floor(tonumber(min) + range * (tonumber(max) - tonumber(min)) + 0.5)
if numVal < 0 then
if plus == "+" then
plus = ""
end
end
return plus .. tostring(numVal)
end)
:gsub("%((%d+%.?%d*)%-(%d+%.?%d*)%)",
function(min, max)
local numVal = m_floor((tonumber(min) + range * (tonumber(max) - tonumber(min))) * 10 + 0.5) / 10
return tostring(numVal)
end)
:gsub("%-(%d+%%) increased", function(num) return num.." reduced" end)
end
-- Clean item text by removing or replacing unsupported or redundant characters or sequences
function itemLib.sanitiseItemText(text)
-- Something something unicode support something grumble
return text:gsub("^%s+",""):gsub("%s+$",""):gsub("\r\n","\n"):gsub("%b<>",""):gsub("","-"):gsub("\226\128\147","-"):gsub("\226\136\146","-"):gsub("ö","o"):gsub("\195\182","o"):gsub("[\128-\255]","?")
end
-- Make an item from raw data
function itemLib.makeItemFromRaw(raw)
local newItem = {
raw = itemLib.sanitiseItemText(raw)
}
itemLib.parseItemRaw(newItem)
if newItem.baseName then
return newItem
end
end
-- Parse raw item data and extract item name, base type, quality, and modifiers
function itemLib.parseItemRaw(item)
item.name = "?"
item.rarity = "UNIQUE"
item.quality = 0
item.rawLines = { }
for line in string.gmatch(item.raw .. "\r\n", "([^\r\n]*)\r?\n") do
line = line:gsub("^%s+",""):gsub("%s+$","")
if #line > 0 then
t_insert(item.rawLines, line)
end
end
local mode = "WIKI"
local l = 1
if item.rawLines[l] then
local rarity = item.rawLines[l]:match("^Rarity: (%a+)")
if rarity then
mode = "GAME"
if data.colorCodes[rarity:upper()] then
item.rarity = rarity:upper()
end
l = l + 1
end
end
if item.rawLines[l] then
item.name = item.rawLines[l]
l = l + 1
end
if item.rarity == "NORMAL" or item.rarity == "MAGIC" then
for baseName, baseData in pairs(data.itemBases) do
if item.name:find(baseName, 1, true) then
item.baseName = baseName
item.type = baseData.type
break
end
end
elseif item.rawLines[l] and not item.rawLines[l]:match("^%-") and data.itemBases[item.rawLines[l]] then
item.baseName = item.rawLines[l]
item.title = item.name
item.name = item.title .. ", " .. item.baseName
item.type = data.itemBases[item.baseName].type
l = l + 1
end
item.base = data.itemBases[item.baseName]
item.modLines = { }
item.implicitLines = 0
item.buffLines = 0
item.affixes = data.itemMods[item.base and item.base.type]
item.prefixes = { }
item.suffixes = { }
local flaskBuffLines = { }
if item.base and item.base.flask and item.base.flask.buff then
item.buffLines = #item.base.flask.buff
for _, line in ipairs(item.base.flask.buff) do
flaskBuffLines[line] = true
local modList, extra = modLib.parseMod(line)
t_insert(item.modLines, { line = line, extra = extra, modList = modList or { }, buff = true })
end
end
local gameModeStage = "FINDIMPLICIT"
local gameModeSection = 1
local foundExplicit
while item.rawLines[l] do
local line = item.rawLines[l]
if flaskBuffLines[line] then
flaskBuffLines[line] = nil
elseif line == "--------" then
gameModeSection = gameModeSection + 1
if gameModeStage == "IMPLICIT" then
item.implicitLines = #item.modLines - item.buffLines
gameModeStage = "FINDEXPLICIT"
elseif gameModeStage == "EXPLICIT" then
gameModeStage = "DONE"
end
elseif line == "Corrupted" then
item.corrupted = true
else
local specName, specVal = line:match("^([%a ]+): (%x+)$")
if not specName then
specName, specVal = line:match("^([%a ]+): %+?([%d%-%.]+)")
end
if not specName then
specName, specVal = line:match("^([%a ]+): (.+)$")
end
if specName then
if specName == "Unique ID" then
item.uniqueID = specVal
elseif specName == "Item Level" then
item.itemLevel = tonumber(specVal)
elseif specName == "Quality" then
item.quality = tonumber(specVal)
elseif specName == "Sockets" then
local group = 0
item.sockets = { }
for c in specVal:gmatch(".") do
if c:match("[RGBW]") then
t_insert(item.sockets, { color = c, group = group })
elseif c == " " then
group = group + 1
end
end
elseif specName == "Radius" and item.type == "Jewel" then
for index, data in pairs(data.jewelRadius) do
if specVal == data.label then
item.jewelRadiusIndex = index
break
end
end
elseif specName == "Limited to" and item.type == "Jewel" then
item.limit = tonumber(specVal)
elseif specName == "Variant" then
if not item.variantList then
item.variantList = { }
end
t_insert(item.variantList, specVal)
elseif specName == "Selected Variant" then
item.variant = tonumber(specVal)
elseif specName == "League" then
item.league = specVal
elseif specName == "Crafted" then
item.crafted = true
elseif specName == "Prefix" then
t_insert(item.prefixes, specVal)
elseif specName == "Suffix" then
t_insert(item.suffixes, specVal)
elseif specName == "Implicits" then
item.implicitLines = tonumber(specVal)
gameModeStage = "EXPLICIT"
elseif specName == "Unreleased" then
item.unreleased = (specVal == "true")
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 rangeSpec = line:match("{range:([%d.]+)}")
local crafted = line:match("{crafted}")
line = line:gsub("%b{}", "")
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(rangedLine or line)
if (not modList or extra) and item.rawLines[l+1] then
-- Try to combine it with the next line
modList, extra = modLib.parseMod(line.." "..item.rawLines[l+1])
if modList and not extra then
line = line.."\n"..item.rawLines[l+1]
l = l + 1
else
modList, extra = modLib.parseMod(rangedLine or line)
end
end
if modList then
t_insert(item.modLines, { line = line, extra = extra, modList = modList, variantList = variantList, crafted = crafted, 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(item.modLines, { line = line, extra = line, modList = { }, variantList = variantList, crafted = crafted })
elseif gameModeStage == "FINDEXPLICIT" then
gameModeStage = "DONE"
end
elseif foundExplicit then
t_insert(item.modLines, { line = line, extra = line, modList = { }, variantList = variantList, crafted = crafted })
end
end
end
l = l + 1
end
if item.base and item.base.implicit then
if item.implicitLines == 0 then
item.implicitLines = 1
end
elseif mode == "GAME" and not foundExplicit then
item.implicitLines = 0
end
item.affixLimit = 0
if item.crafted and item.affixes then
if item.rarity == "MAGIC" then
item.affixLimit = 2
elseif item.rarity == "RARE" then
item.affixLimit = (item.base.type == "Jewel" and 4 or 6)
end
end
if item.variantList then
item.variant = m_min(#item.variantList, item.variant or #item.variantList)
end
itemLib.buildItemModList(item)
end
function itemLib.normaliseQuality(item)
if not item.corrupted and not item.uniqueID and item.base and (item.base.armour or item.base.weapon or item.base.flask) then
item.quality = 20
end
end
-- Create raw item data for given item
function itemLib.createItemRaw(item)
local rawLines = { }
t_insert(rawLines, "Rarity: "..item.rarity)
if item.title then
t_insert(rawLines, item.title)
t_insert(rawLines, item.baseName)
else
t_insert(rawLines, item.name)
end
if item.uniqueID then
t_insert(rawLines, "Unique ID: "..item.uniqueID)
end
if item.league then
t_insert(rawLines, "League: "..item.league)
end
if item.unreleased then
t_insert(rawLines, "Unreleased: true")
end
if item.crafted then
t_insert(rawLines, "Crafted: true")
for _, name in ipairs(item.prefixes or { }) do
t_insert(rawLines, "Prefix: "..name)
end
for _, name in ipairs(item.suffixes or { }) do
t_insert(rawLines, "Suffix: "..name)
end
end
if item.itemLevel then
t_insert(rawLines, "Item Level: "..item.itemLevel)
end
if item.variantList then
for _, variantName in ipairs(item.variantList) do
t_insert(rawLines, "Variant: "..variantName)
end
t_insert(rawLines, "Selected Variant: "..item.variant)
end
if item.quality > 0 then
t_insert(rawLines, "Quality: "..item.quality)
end
if item.sockets then
local line = "Sockets: "
for i, socket in pairs(item.sockets) do
line = line .. socket.color
if item.sockets[i+1] then
line = line .. (socket.group == item.sockets[i+1].group and "-" or " ")
end
end
t_insert(rawLines, line)
end
if item.jewelRadiusIndex then
t_insert(rawLines, "Radius: "..data.jewelRadius[item.jewelRadiusIndex].label)
end
if item.limit then
t_insert(rawLines, "Limited to: "..item.limit)
end
t_insert(rawLines, "Implicits: "..item.implicitLines)
for _, modLine in ipairs(item.modLines) do
if not modLine.buff then
local line = modLine.line
if modLine.range then
line = "{range:"..round(modLine.range,2).."}" .. line
end
if modLine.crafted then
line = "{crafted}" .. 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 item.corrupted then
t_insert(rawLines, "Corrupted")
end
return table.concat(rawLines, "\n")
end
-- Rebuild explicit modifiers using the item's affixes
function itemLib.craftItem(item)
local ranges = { }
for l = item.buffLines + item.implicitLines + 1, #item.modLines do
ranges[item.modLines[l].line] = item.modLines[l].range
item.modLines[l] = nil
end
local newName = item.baseName
for _, list in ipairs({item.prefixes,item.suffixes}) do
for i = 1, item.affixLimit/2 do
local name = list[i]
if not name then
list[i] = "None"
end
local mod = item.affixes[name]
if mod then
if mod.type == "Prefix" then
newName = name .. " " .. newName
elseif mod.type == "Suffix" then
newName = newName .. " " .. name
end
for _, line in ipairs(mod) do
t_insert(item.modLines, { line = line, range = ranges[line] })
end
end
end
end
if item.rarity == "MAGIC" then
item.name = newName
end
item.raw = itemLib.createItemRaw(item)
itemLib.parseItemRaw(item)
end
-- Return the name of the slot this item is equipped in
function itemLib.getPrimarySlotForItem(item)
if item.base.weapon then
return "Weapon 1"
elseif item.type == "Quiver" or item.type == "Shield" then
return "Weapon 2"
elseif item.type == "Ring" then
return "Ring 1"
elseif item.type == "Flask" then
return "Flask 1"
else
return item.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 = 0
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.tagList[1] or mod.tagList[1].type == "InSlot") then
result = result + mod.value
t_remove(modList, i)
else
i = i + 1
end
end
return result
end
-- Build list of modifiers for an item in a given slot number (1 or 2) while applying local modifers and adding quality
function itemLib.buildItemModListForSlotNum(item, baseList, slotNum)
local slotName = itemLib.getPrimarySlotForItem(item)
if slotNum == 2 then
slotName = slotName:gsub("1", "2")
end
local modList = common.New("ModList")
for _, baseMod in ipairs(baseList) do
local mod = copyTable(baseMod)
local add = true
for _, tag in pairs(mod.tagList) do
if tag.type == "SlotNumber" or tag.type == "InSlot" then
if tag.num ~= slotNum then
add = false
break
end
elseif tag.type == "SocketedIn" then
tag.slotName = slotName
elseif tag.type == "Condition" and tag.var == "XHandAttack" then
tag.var = (slotNum == 1) and "MainHandAttack" or "OffHandAttack"
end
end
if add then
mod.sourceSlot = slotName
modList:AddMod(mod)
end
end
if item.base.weapon then
local weaponData = { }
item.weaponData[slotNum] = weaponData
weaponData.type = item.base.type
weaponData.name = item.name
weaponData.AttackSpeedInc = sumLocal(modList, "Speed", "INC", ModFlag.Attack)
weaponData.attackRate = round(item.base.weapon.attackRateBase * (1 + weaponData.AttackSpeedInc / 100), 2)
for _, dmgType in pairs(dmgTypeList) do
local min = (item.base.weapon[dmgType.."Min"] or 0) + sumLocal(modList, dmgType.."Min", "BASE", 0)
local max = (item.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 + item.quality) / 100))
max = round(max * (1 + (physInc + item.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(item.base.weapon.critChanceBase * (1 + sumLocal(modList, "CritChance", "INC", 0) / 100), 2)
for _, value in ipairs(modList:Sum("LIST", nil, "Misc")) do
if value.type == "WeaponData" then
weaponData[value.key] = value.value
end
end
for _, mod in ipairs(modList) do
-- Convert accuracy modifiers to local
if mod.name == "Accuracy" and mod.flags == 0 and mod.keywordFlags == 0 and not mod.tagList[1] then
mod.tagList[1] = { 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 item.base.armour then
local armourData = item.armourData
local armourBase = sumLocal(modList, "Armour", "BASE", 0) + (item.base.armour.armourBase or 0)
local evasionBase = sumLocal(modList, "Evasion", "BASE", 0) + (item.base.armour.evasionBase or 0)
local energyShieldBase = sumLocal(modList, "EnergyShield", "BASE", 0) + (item.base.armour.energyShieldBase or 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 * (1 + (armourInc + armourEvasionInc + armourEnergyShieldInc + defencesInc + item.quality) / 100))
armourData.Evasion = round(evasionBase * (1 + (evasionInc + armourEvasionInc + evasionEnergyShieldInc + defencesInc + item.quality) / 100))
armourData.EnergyShield = round(energyShieldBase * (1 + (energyShieldInc + armourEnergyShieldInc + evasionEnergyShieldInc + defencesInc + item.quality) / 100))
if item.base.armour.blockChance then
armourData.BlockChance = item.base.armour.blockChance + sumLocal(modList, "BlockChance", "BASE", 0)
end
if item.base.armour.movementPenalty then
modList:NewMod("MovementSpeed", "INC", -item.base.armour.movementPenalty, item.modSource, { type = "Condition", var = "IgnoreMovementPenalties", neg = true })
end
for _, value in ipairs(modList:Sum("LIST", nil, "Misc")) do
if value.type == "ArmourData" then
armourData[value.key] = value.value
end
end
elseif item.base.flask then
local flaskData = item.flaskData
local durationInc = sumLocal(modList, "Duration", "INC", 0)
if item.base.flask.life or item.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 = item.base.flask.duration * (1 + durationInc / 100) / rateMod
if item.base.flask.life then
flaskData.lifeBase = item.base.flask.life * (1 + item.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 item.base.flask.mana then
flaskData.manaBase = item.base.flask.mana * (1 + item.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 = item.base.flask.duration * (1 + (durationInc + item.quality) / 100)
end
flaskData.chargesMax = item.base.flask.chargesMax + sumLocal(modList, "FlaskCharges", "BASE", 0)
flaskData.chargesUsed = m_floor(item.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:Sum("LIST", nil, "Misc")) do
if value.type == "FlaskData" then
flaskData[value.key] = value.value
end
end
elseif item.type == "Jewel" then
local jewelData = item.jewelData
for _, value in ipairs(modList:Sum("LIST", nil, "Misc")) do
if value.type == "JewelFunc" then
jewelData.funcList = jewelData.funcList or { }
t_insert(jewelData.funcList, value.func)
elseif value.type == "JewelData" then
jewelData[value.key] = value.value
end
end
end
return { unpack(modList) }
end
-- Build lists of modifiers for each slot an item can occupy
function itemLib.buildItemModList(item)
if not item.base then
return
end
local baseList = { }
item.baseModList = baseList
item.rangeLineList = { }
item.modSource = "Item:"..(item.id or -1)..":"..item.name
for _, modLine in ipairs(item.modLines) do
if not modLine.extra and (not modLine.variantList or modLine.variantList[item.variant]) then
if modLine.range then
local line = itemLib.applyRange(modLine.line, modLine.range)
local list, extra = modLib.parseMod(line)
if list and not extra then
modLine.modList = list
t_insert(item.rangeLineList, modLine)
end
end
for _, mod in ipairs(modLine.modList) do
mod.source = item.modSource
if type(mod.value) == "table" and mod.value.mod then
mod.value.mod.source = mod.source
end
t_insert(baseList, mod)
end
end
end
if item.name == "Tabula Rasa, Simple Robe" or item.name == "Skin of the Loyal, Simple Robe" or item.name == "Skin of the Lords, Simple Robe" then
-- Hack to remove the energy shield
t_insert(baseList, { name = "Misc", type = "LIST", value = { type = "ArmourData", key = "EnergyShield" }, flags = 0, keywordFlags = 0, tagList = { } })
end
if item.base.weapon then
item.weaponData = { }
elseif item.base.armour then
item.armourData = { }
elseif item.base.flask then
item.flaskData = { }
elseif item.type == "Jewel" then
item.jewelData = { }
end
if item.base.weapon or item.type == "Ring" then
item.slotModList = { }
for i = 1, 2 do
item.slotModList[i] = itemLib.buildItemModListForSlotNum(item, baseList, i)
end
else
item.modList = itemLib.buildItemModListForSlotNum(item, baseList)
end
end