Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions spec/System/TestItemParse_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,13 @@
assert.truthy(item.explicitModLines[1].synthesis)
end)

it("unscalable", function()
local item = new("Item", raw("{unscalable}+8 to Strength"))
assert.truthy(item.explicitModLines[1].unscalable)
item = new("Item", raw("+8 to Strength - Unscalable Value"))
assert.truthy(item.explicitModLines[1].unscalable)
end)

it("multiple bases", function()
local item = new("Item", [[
Ashcaller
Expand Down Expand Up @@ -465,3 +472,160 @@
assert.are.equal("+1500 to Armour", item.buffModLines[1].line)
end)
end)

describe("TestAdvancedItemParse #item", function()
local function raw(s, base)
base = base or "Plate Vest"
return "Rarity: Rare\nName\n"..base.."\n"..s
end

it("parses to craft", function()
local item = new("Item", raw([[
{ Prefix Modifier "Fecund" (Tier: 1) — Life }
+142(130-144) to maximum Life
]], "Cord Belt"))
assert.are.equals("IncreasedLife9", item.prefixes[1].modId)
assert.are.equals(0.857, item.prefixes[1].range)
assert.are.equals("life", item.explicitModLines[1].modTags[1])
item = new("Item", raw([[
{ Master Crafted Suffix Modifier "of Craft" (Rank: 3) — Elemental, Cold, Resistance }
+35(29-35)% to Cold Resistance
]], "Cord Belt"))
assert.truthy(item.explicitModLines[1].crafted)
end)

it("parses correct range", function()
local item = new("Item", raw([[
{ Prefix Modifier "Freezing" (Tier: 5) — Damage, Elemental, Cold, Caster — 8% Increased }
Adds 17(16-20) to 35(30-36) Cold Damage to Spells
]], "Void Sceptre"))
assert.are.equals("Adds 17 to 35 Cold Damage to Spells", item.explicitModLines[1].line)
end)

-- GGG scales each mod line separately here, but PoB scales them both together, so this parsing is a bit wonky
it("parses multi-line mod", function()
local item = new("Item", raw([[
{ Prefix Modifier "Warlock's" (Tier: 4) — Mana, Damage, Caster }
32(30-37)% increased Spell Damage
+46(42-47) to maximum Mana
]], "Royal Staff"))
assert.are.equals("SpellDamageAndManaOnTwoHandWeapon4", item.prefixes[1].modId)
assert.are.equals(0.286, item.prefixes[1].range)
assert.are.equals(0.8, item.explicitModLines[2].range)
end)

it("resets linePrefix", function()
local item = new("Item", raw([[
{ Prefix Modifier "Warlock's" (Tier: 4) — Mana, Damage, Caster }
32(30-37)% increased Spell Damage
+46(42-47) to maximum Mana
--------
+15 to maximum life
]], "Royal Staff"))
assert.are_not.equals("mana", item.explicitModLines[3].modTags[1])
end)

it("parses vaaled catalyst", function()

Check warning on line 528 in spec/System/TestItemParse_spec.lua

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (vaaled)
local item = new("Item", raw([[
Quality (Attribute Modifiers): +19% (augmented)
{ Unique Modifier — Attribute — 19% Increased }
+120(80-100) to all Attributes
(Attributes are Strength, Dexterity, and Intelligence)
]], "Onyx Amulet"))
assert.are.equals(142, item.baseModList[1].value)
-- assert.falsy(item.explicitModLines[1].range) -- Not sure why this is returning 0.5
assert.are.equals(6, item.catalyst)
assert.are.equals(19, item.catalystQuality)
end)

it("parses vaaled catalyst within range", function()

Check warning on line 541 in spec/System/TestItemParse_spec.lua

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (vaaled)
local item = new("Item", raw([[
Quality (Attribute Modifiers): +19% (augmented)
{ Unique Modifier — Attribute — 19% Increased }
+95(80-100) to all Attributes
(Attributes are Strength, Dexterity, and Intelligence)
]], "Onyx Amulet"))
assert.are.equals(113, item.baseModList[1].value)
assert.are.equals(0.75, item.explicitModLines[1].range)
assert.are.equals(6, item.catalyst)
assert.are.equals(19, item.catalystQuality)
end)

it("doesn't scale unscalable", function()
local item = new("Item", raw([[
Quality (Life and Mana Modifiers): +20% (augmented)
{ Unique Modifier — Life, Defences, Energy Shield, Minion, Gem }
Socketed Golem Skills gain 20% of Maximum Life as Extra Maximum Energy Shield — Unscalable Value
]]))
assert.are.equals(20, item.baseModList[1].value.mod.value)
end)

it("parses junk", function()
local godTestItem = new("Item", [[
Item Class: Sceptres
Rarity: Unique
Nebulis
Synthesised Void Sceptre
--------
Sceptre
Physical Damage: 50-76
Critical Strike Chance: 7.30%
Attacks per Second: 1.25
Weapon Range: 1.1 metres
Memory Strands: 58
--------
Requirements:
Level: 68
Str: 104
Int: 122
--------
Sockets: B R
--------
Item Level: 87
--------
+30% to Fire Resistance (scourge)
22% reduced Global Defences (scourge)
(Armour, Evasion Rating and Energy Shield are the standard Defences) (scourge)
--------
8% increased Explicit Cold Modifier magnitudes (enchant)
Has 1 White Socket (enchant)
--------
{ Searing Exarch Implicit Modifier (Lesser) }
Tempest Shield has 15(15-17)% increased Buff Effect
{ Implicit Modifier — Damage, Critical — 106% Increased }
+15(15-17)% to Global Critical Strike Multiplier
--------
{ Prefix Modifier "Freezing" (Tier: 5) — Damage, Elemental, Cold, Caster — 8% Increased }
Adds 17(16-20) to 35(30-36) Cold Damage to Spells
{ Prefix Modifier "Beetle's" (Tier: 6) — Defences, Armour }
9(6-13)% increased Armour
7(6-7)% increased Stun and Block Recovery
{ Master Crafted Prefix Modifier "Upgraded" — Life, Defences, Armour }
21(18-21)% increased Armour
+18(17-19) to maximum Life
{ Unique Modifier }
106(60-120)% increased Implicit Modifier magnitudes — Unscalable Value
(Implicit Modifiers are those that come from an item's type, rather than its random properties)
{ Master Crafted Suffix Modifier "of Craft" (Rank: 3) — Elemental, Cold, Resistance }
+35(29-35)% to Cold Resistance
{ Fractured Prefix Modifier "Thorny" (Tier: 2) — Damage, Physical }
Reflects 3(1-4) Physical Damage to Melee Attackers
{ Prefix Modifier "Veiled" }
Veiled Prefix
Searing Exarch Item
--------
{ Allocated Crucible Passive Skill (Tier: 2) }
Adds 2 to 6 Physical Damage to Spells
--------
Synthesised Item
--------
Corrupted
--------
Scourged
--------
Hinekora's Lock
--------
Note: ~b/o 2 chaos
]])
end)
end)
125 changes: 122 additions & 3 deletions src/Classes/Item.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ local m_floor = math.floor

local dmgTypeList = {"Physical", "Lightning", "Cold", "Fire", "Chaos"}
local catalystList = {"Abrasive", "Accelerating", "Dextral", "Fertile", "Imbued", "Intrinsic", "Noxious", "Prismatic", "Sinistral", "Tempering", "Turbulent", "Unstable"}
local catalystDescriptorList = {"Attack", "Speed", "Suffix", "Life and Mana", "Caster", "Attribute", "Physical and Chaos", "Resistance", "Prefix", "Defense", "Elemental", "Critical"}
local catalystTags = {
{ "attack" },
{ "speed" },
Expand All @@ -28,6 +29,9 @@ local catalystTags = {
}

local function getCatalystScalar(catalystId, mod, quality)
if mod.unscalable then
return 1
end
local tags = mod.modTags
local affixType = mod.type
if not catalystId or type(catalystId) ~= "number" or not catalystTags[catalystId] or not tags or type(tags) ~= "table" or #tags == 0 then
Expand Down Expand Up @@ -78,7 +82,7 @@ end
local lineFlags = {
["crafted"] = true, ["crucible"] = true, ["custom"] = true, ["eater"] = true, ["enchant"] = true,
["exarch"] = true, ["fractured"] = true, ["implicit"] = true, ["scourge"] = true, ["synthesis"] = true,
["mutated"] = true
["mutated"] = true, ["unscalable"] = true
}

-- Special function to store unique instances of modifier on specific item slots
Expand Down Expand Up @@ -376,6 +380,7 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
local deferJewelRadiusIndexAssignment
local gameModeStage = "FINDIMPLICIT"
local foundExplicit, foundImplicit
local linePrefix = ""

while self.rawLines[l] do
local line = self.rawLines[l]
Expand All @@ -387,6 +392,7 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
elseif tinctureBuffLines and tinctureBuffLines[line] then
tinctureBuffLines[line] = nil
elseif line == "--------" then
linePrefix = ""
self.checkSection = true
elseif line == "Split" then
self.split = true
Expand All @@ -405,7 +411,44 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
self[influenceItemMap[line]] = true
elseif line == "Requirements:" then
-- nothing to do
elseif line:match("^%(") then
-- Reminder text, nothing to parse
elseif line:match("^{ ") then
-- We're parsing advanced copy/paste format
linePrefix = ""
self.crafted = true
local fullModName, modTags, increasedAmt = line:match("^{ (.-) %- (.-) %- (%d*).*}$")
if not fullModName then
fullModName, modTags = line:match("^{ (.-) %- (.-) }$")
end
if not fullModName then
fullModName = line:match("^{ (.-) }$")
end
local modName = fullModName:match("^.*Modifier \"(.*)\"")
if modName and modName ~= "" then
for modId, modData in pairs(self.affixes) do
if modData.affix == modName and self:CanHaveMod(modData) then
if modData.type == "Prefix" then
self.pendingAffix = { modId = modId, table = self.prefixes }
elseif modData.type == "Suffix" then
self.pendingAffix = { modId = modId, table = self.suffixes }
end
end
end
end
local possibleLineFlags = fullModName:match("(.*)Modifier.*")
if possibleLineFlags then
for flag in possibleLineFlags:gmatch("%a+") do
if lineFlags[flag:lower()] then
linePrefix = linePrefix .. "{" .. flag:lower() .. "}"
end
end
end
if modTags and modTags ~= "" then
linePrefix = linePrefix .. "{tags:" .. modTags:lower():gsub("%s+", "") .. "}"
end
else
line = linePrefix .. line
if self.checkSection then
if gameModeStage == "IMPLICIT" then
if foundImplicit then
Expand All @@ -424,7 +467,7 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
end
self.checkSection = false
end
local specName, specVal = line:match("^([%a ]+:?): (.+)$")
local specName, specVal = line:match("^([%a %(%)]+:?): (.+)$")
if specName then
if specName == "Class:" then
specName = "Requires Class"
Expand All @@ -439,6 +482,13 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
self.itemLevel = specToNumber(specVal)
elseif specName == "Requires Class" then
self.classRestriction = specVal
elseif specName:match("Quality %(%a+ Modifiers%)") then
self.catalystQuality = specToNumber(specVal:match("(%d+)%%"))
for i=1, #catalystDescriptorList do
if specName:match("Quality %(([%a%s]+) Modifiers%)") == catalystDescriptorList[i] then
self.catalyst = i
end
end
elseif specName == "Quality" then
self.quality = specToNumber(specVal)
elseif specName == "Sockets" then
Expand Down Expand Up @@ -739,7 +789,51 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
foundImplicit = true
gameModeStage = "IMPLICIT"
end
local catalystScalar = getCatalystScalar(self.catalyst, modLine, self.catalystQuality)
local catalystScalar = 1
if line:match(" %- Unscalable Value$") then
line = line:gsub(" %- Unscalable Value$", "")
modLine.unscalable = true
else
catalystScalar = getCatalystScalar(self.catalyst, modLine, self.catalystQuality)
end
if self.pendingAffix then
local bestPrecisionDelta = 0
local bestPrecisionRange = 0
for value, range in line:gmatch("(%d+)%((%d+%-%d+)%)") do
-- Find advanced copy paste format: 45(40-50)
local min, max = range:match("(%d+)%-(%d+)")
local delta = tonumber(max) - min
line = line:gsub(value .. "%(" .. range:gsub("%-", "%%-") .. "%)", value)
if delta > bestPrecisionDelta then
bestPrecisionRange = round((value - min) / delta, 3)
bestPrecisionDelta = delta
end
end
t_insert(self.pendingAffix.table, {
modId = self.pendingAffix.modId,
range = tonumber(bestPrecisionRange),
})
self.pendingAffix = nil
else
local bestPrecisionDelta = 0
local bestPrecisionRange = 0
for value, range in line:gmatch("(%d+)%((%d+%-%d+)%)") do
local min, max = range:match("(%d+)%-(%d+)")
local delta = tonumber(max) - min
if delta > bestPrecisionDelta then
bestPrecisionRange = round((value - min) / delta, 3)
bestPrecisionDelta = delta
end
if bestPrecisionRange > 1 or bestPrecisionRange < 0 then
line = line:gsub(value .. "%(" .. range:gsub("%-", "%%-") .. "%)", value)
else
line = line:gsub(value .. "%(" .. range:gsub("%-", "%%-") .. "%)", "(" .. range .. ")")
end
end
if bestPrecisionRange < 1 and bestPrecisionRange > 0 then
modLine.range = tonumber(bestPrecisionRange)
end
end
local rangedLine = itemLib.applyRange(line, 1, catalystScalar)
local modList, extra = modLib.parseMod(rangedLine)
if (not modList or extra) and self.rawLines[l+1] then
Expand Down Expand Up @@ -1141,6 +1235,9 @@ function ItemClass:BuildRaw()
if modLine.synthesis then
line = "{synthesis}" .. line
end
if modLine.unscalable then
line = "{unscalable}" .. line
end
if modLine.variantList then
local varSpec
for varId in pairs(modLine.variantList) do
Expand Down Expand Up @@ -1837,3 +1934,25 @@ function ItemClass:BuildModList()
self.modList = self:BuildModListForSlotNum(baseList)
end
end

function ItemClass:CanHaveMod(mod)
local keyMap, includeTags = { }, { }
for index, key in ipairs(mod.weightKey) do
keyMap[key] = index
end
-- check for uniques with off-tag mods
if data.casterTagCrucibleUniques[self.title] then
includeTags["caster_unique_weapon"] = true
end
if data.minionTagCrucibleUniques[self.title] then
includeTags["minion_unique_weapon"] = true
end
if self.canHaveOnlySupportSkillsCrucibleTree then
return keyMap["crucible_unique_staff"] and mod.weightVal[keyMap["crucible_unique_staff"]] ~= 0
elseif self.canHaveShieldCrucibleTree then
return self:GetModSpawnWeight(mod, { ["crucible_unique_helmet"] = true, ["shield"] = true }) > 0
elseif self.canHaveTwoHandedSwordCrucibleTree then
return self:GetModSpawnWeight(mod, { ["two_hand_weapon"] = true }, { ["one_hand_weapon"] = true }) > 0
end
return self:GetModSpawnWeight(mod, includeTags) > 0
end
Loading
Loading