From 2300dc2062c0b56d11832ef5a080dd2e73a23f4e Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Sun, 30 Aug 2026 11:12:40 +0000 Subject: [PATCH 1/3] Apply changes from https://github.com/PathOfBuildingCommunity/PathOfBuilding/pull/10039 --- spec/System/TestItemParse_spec.lua | 271 +++++++++++++++++++++++++ spec/System/TestItemParse_spec.lua.rej | 15 ++ src/Classes/Item.lua | 44 +++- src/Classes/Item.lua.rej | 112 ++++++++++ src/Classes/ItemsTab.lua | 10 +- src/Classes/ItemsTab.lua.rej | 19 ++ src/Data/ModCache.lua.rej | 9 + src/Modules/ItemTools.lua.rej | 15 ++ 8 files changed, 484 insertions(+), 11 deletions(-) create mode 100644 spec/System/TestItemParse_spec.lua.rej create mode 100644 src/Classes/Item.lua.rej create mode 100644 src/Classes/ItemsTab.lua.rej create mode 100644 src/Data/ModCache.lua.rej create mode 100644 src/Modules/ItemTools.lua.rej diff --git a/spec/System/TestItemParse_spec.lua b/spec/System/TestItemParse_spec.lua index 37f90839c6..50328b1021 100644 --- a/spec/System/TestItemParse_spec.lua +++ b/spec/System/TestItemParse_spec.lua @@ -1262,6 +1262,232 @@ describe("TestAdvancedItemParse #item", function() Note: ~b/o 2 chaos ]]) end) + + it("parses allocated Crucible passive skills from advanced copy", function() + local item = new("Item", raw([[ + { Allocated Crucible Passive Skill (Tier: 1) } + -3% to Critical Strike Chance + +100% to Global Critical Strike Multiplier + { Allocated Crucible Passive Skill (Tier: 1) } + Rampage + (You gain Rampage bonuses for Killing multiple Enemies in quick succession) + ]], "Citadel Bow")) + + assert.are.equals(3, #item.crucibleModLines) + assert.are.equals(0, #item.explicitModLines) + assert.are.same({ + "-3% to Critical Strike Chance", + "+100% to Global Critical Strike Multiplier", + "Rampage", + }, lines(item.crucibleModLines)) + end) + + it("ignores attribute requirements from socketed gems", function() + local item = new("Item", raw([[ + Requirements: + Str: 126 (unmet) + Dex: 185 (unmet) + Int: 129 (unmet) + -------- + Sockets: W-W-W-W-W-W + -------- + Item Level: 83 + ]], "Citadel Bow")) + + assert.are.same({ str = 0, dex = 185, int = 0 }, { + str = item.requirements.strMod, + dex = item.requirements.dexMod, + int = item.requirements.intMod, + }) + end) + + it("orders fractured mods first and crafted mods last", function() + local item = new("Item", raw([[ + Item Level: 83 + { Fractured Prefix Modifier "Cheetah's" (Tier: 2) — Speed } + 30% increased Movement Speed + { Prefix Modifier "Athlete's" (Tier: 1) — Life } + +128(115-129) to maximum Life + { Master Crafted Prefix Modifier "Upgraded" (Rank: 2) — Mana } + +43(35-44) to maximum Mana + { Suffix Modifier "of the Jaguar" (Tier: 3) — Attribute } + +41(38-42) to Dexterity + ]], "Dragonscale Boots")) + local expectedLines = { + "30% increased Movement Speed", + "+41 to Dexterity", + "+128 to maximum Life", + "+(35-44) to maximum Mana", + } + assert.are.same(expectedLines, lines(item.explicitModLines)) + + item:Craft() + item:Craft() + assert.are.same(expectedLines, lines(item.explicitModLines)) + end) + + it("matches same-name affixes using their advanced-copy ranges", function() + local item = new("Item", raw([[ + Item Level: 85 + { Fractured Prefix Modifier "Essences" — Damage, Elemental, Fire, Attack } + Adds 100(80-109) to 179(162-189) Fire Damage + { Prefix Modifier "Essences" — Damage, Elemental, Lightning, Attack } + Adds 14(13-19) to 285(266-310) Lightning Damage + ]], "Kinetic Wand")) + + assert.are.equals("LocalAddedFireDamageEssence7", item.prefixes[1].modId) + assert.are.equals("LocalAddedLightningDamageEssence7_", item.prefixes[2].modId) + + item:Craft() + item:Craft() + assert.are.equals("Adds 100 to 179 Fire Damage", item.explicitModLines[1].line) + assert.are.equals("Adds 14 to 285 Lightning Damage", item.explicitModLines[2].line) + end) + + it("filters flask base properties and parses fixed-value advanced rolls", function() + local item = new("Item", [[ + Rarity: Unique + Soul Catcher + Quartz Flask + -------- + Lasts 7.20 (augmented) Seconds + Consumes 30 of 60 Charges on use + Currently has 59 Charges + +10% chance to Suppress Spell Damage + (40% of Damage from Suppressed Hits and Ailments they inflict is prevented) + Phasing + -------- + { Unique Modifier } + Consumes Maximum Charges to use + { Unique Modifier } + Vaal Skills used during effect have 40(10)% reduced Soul Gain Prevention Duration + ]]) + + assert.are.equals(2, #item.buffModLines) + assert.are.equals(0, #item.implicitModLines) + assert.are.equals(2, #item.explicitModLines) + assert.are.equals("Consumes Maximum Charges to use", item.explicitModLines[1].line) + assert.are.equals("Vaal Skills used during effect have 40% reduced Soul Gain Prevention Duration", item.explicitModLines[2].line) + end) + + it("preserves rolls from large advanced-copy ranges", function() + local item = new("Item", [[ + Rarity: Unique + Elegant Hubris + Timeless Jewel + { Unique Modifier } + Commissioned 150720(2000-160000) coins to commemorate Chitus(Cadiro-Victario) + ]]) + + local seedLine = itemLib.applyRange(item.explicitModLines[1].line, item.explicitModLines[1].range) + assert.are.equals("Commissioned 150720 coins to commemorate Chitus", seedLine) + + item:BuildAndParseRaw() + seedLine = itemLib.applyRange(item.explicitModLines[1].line, item.explicitModLines[1].range) + assert.are.equals("Commissioned 150720 coins to commemorate Chitus", seedLine) + end) + + it("preserves independently rolled values on the same modifier line", function() + local item = new("Item", [[ + Rarity: Unique + Prismweave + Rustic Sash + { Unique Modifier — Damage, Elemental, Fire, Attack } + Adds 16(14-16) to 32(30-32) Fire Damage to Attacks + { Unique Modifier — Damage, Elemental, Cold, Attack } + Adds 10(10-12) to 27(24-28) Cold Damage to Attacks + ]]) + + assert.are.equals("Adds (14-16) to (30-32) Fire Damage to Attacks", item.explicitModLines[1].line) + assert.are.equals("Adds 10 to 27 Cold Damage to Attacks", item.explicitModLines[2].line) + end) + + it("orders advanced-copy unique modifiers by their database stat order", function() + local item = new("Item", [[ + Rarity: Unique + Geofri's Sanctuary + Elegant Ringmail + { Unique Modifier — Life } + +66(60-70) to maximum Life + { Unique Modifier — Defences, Energy Shield } + +31(30-40) to maximum Energy Shield + { Unique Modifier — Defences, Armour, Energy Shield } + 63(50-75)% increased Armour and Energy Shield + { Unique Modifier — Life, Defences, Energy Shield } + Zealot's Oath + { Unique Modifier — Defences, Energy Shield } + +2 maximum Energy Shield per 5 Strength + { Unique Modifier — Elemental, Resistance } + +18(14-18)% to all Elemental Resistances + ]]) + + assert.are.same({ + "(50-75)% increased Armour and Energy Shield", + "+(30-40) to maximum Energy Shield", + "+(60-70) to maximum Life", + "+(14-18)% to all Elemental Resistances", + "+2 maximum Energy Shield per 5 Strength", + "Zealot's Oath", + }, lines(item.explicitModLines)) + end) + + it("keeps the selected value from advanced-copy enum ranges", function() + local item = new("Item", [[ + Rarity: Unique + The Dark Monarch + Lich's Circlet + { Unique Modifier } + Maximum number of Raised Zombies (Animated Weapons-Holy Armaments) is Doubled + Cannot have Minions other than Raised Zombies (Animated Weapons-Holy Armaments) + ]]) + + assert.are.equals("Maximum number of Raised Zombies is Doubled", item.explicitModLines[1].line) + assert.are.equals("Cannot have Minions other than Raised Zombies", item.explicitModLines[2].line) + assert.is_true(#item.explicitModLines[1].modList > 0) + end) + + it("parses punctuated enum and descending numeric ranges", function() + local gemItem = new("Item", [[ + Rarity: Unique + Replica Dragonfang's Flight + Onyx Amulet + { Unique Modifier } + +3 to Level of all Lightning Tendrils(Fireball-Mana-Infused Staff) Gems + ]]) + assert.are.equals("+3 to Level of all Lightning Tendrils Gems", gemItem.explicitModLines[1].line) + assert.is_true(#gemItem.explicitModLines[1].modList > 0) + + local requirementItem = new("Item", [[ + Rarity: Unique + Replica Dragonfang's Flight + Onyx Amulet + { Unique Modifier } + Items and Gems have 8(10-5)% reduced Attribute Requirements + ]]) + assert.are.equals("Items and Gems have (5-10)% reduced Attribute Requirements", requirementItem.explicitModLines[1].line) + assert.are.equals("Items and Gems have 8% reduced Attribute Requirements", + itemLib.applyRange(requirementItem.explicitModLines[1].line, requirementItem.explicitModLines[1].range)) + end) + + it("parses Memory Strands as an item property", function() + local item = new("Item", [[ + Rarity: Magic + Imperial Maul of Revitalization + Weapon Range: 1.3 metres + Memory Strands: 70 + Item Level: 85 + { Suffix Modifier "of Revitalization" (Tier: 1) — Life, Attack } + Grants 28(27-30) Life per Enemy Hit + ]]) + + assert.are.equals(70, item.memoryStrands) + assert.are.equals(1, #item.explicitModLines) + + item:BuildAndParseRaw() + assert.are.equals(70, item.memoryStrands) + assert.are.equals(1, #item.explicitModLines) + end) + describe("mod magnitude scaling", function() before_each(function() newBuild() @@ -1283,6 +1509,51 @@ describe("TestAdvancedItemParse #item", function() return build.calcsTab.mainEnv.modDB:Sum("INC", { flags = ModFlag.Spell }, "Damage") end + it("scales advanced-copy Simplex Amulet explicit mods on the first parse", function() + local rawItem = [[ + Rarity: Rare + Grim Collar + Simplex Amulet + Quality (Critical Modifiers): +20% (augmented) + { Implicit Modifier } + -2 Prefix Modifiers allowed + -1 Suffix Modifier allowed + 100% increased Explicit Modifier magnitudes + { Prefix Modifier "The Elder's" (Tier: 1) — Damage, Chaos — 100% Increased } + Gain 13(3-5)% of Non-Chaos Damage as extra Chaos Damage + { Suffix Modifier "of Destruction" (Tier: 1) — Damage, Critical — 120% Increased } + +70(35-38)% to Global Critical Strike Multiplier + { Suffix Modifier "of Amassment" — Drop — 100% Increased } + 20(17-20)% increased Quantity of Items found + Shaper Item + Elder Item + ]] + + build.itemsTab:CreateDisplayItemFromRaw(rawItem, true) + local firstItem = build.itemsTab.displayItem + local function findModLine(line) + for _, modLine in ipairs(firstItem.explicitModLines) do + if modLine.line == line then + return modLine + end + end + end + assert.are.equals(2, findModLine("Gain 13% of Non-Chaos Damage as extra Chaos Damage").valueScalar) + assert.are.equals(2.2, findModLine("+70% to Global Critical Strike Multiplier").valueScalar) + + -- Changing a different affix must not normalise either legacy roll. + firstItem.suffixes[2].range = 0 + firstItem:Craft() + assert.are.equals(2, findModLine("Gain 13% of Non-Chaos Damage as extra Chaos Damage").valueScalar) + assert.are.equals(2.2, findModLine("+70% to Global Critical Strike Multiplier").valueScalar) + + -- Editing the legacy affix itself deliberately returns it to the current range. + firstItem.suffixes[1].range = 1 + firstItem:Craft() + assert.are.equals(2, findModLine("Gain 13% of Non-Chaos Damage as extra Chaos Damage").valueScalar) + assert.are.equals(2.2, findModLine("+38% to Global Critical Strike Multiplier").valueScalar) + end) + it("scales matching implicit mods by modifier magnitude", function() -- 130% * 1.7 = 221 build.itemsTab:CreateDisplayItemFromRaw([[ diff --git a/spec/System/TestItemParse_spec.lua.rej b/spec/System/TestItemParse_spec.lua.rej new file mode 100644 index 0000000000..cb3954cf53 --- /dev/null +++ b/spec/System/TestItemParse_spec.lua.rej @@ -0,0 +1,15 @@ +diff a/spec/System/TestItemParse_spec.lua b/spec/System/TestItemParse_spec.lua (rejected hunks) +@@ -506,6 +506,13 @@ describe("TestAdvancedItemParse #item", function() + base = base or "Plate Vest" + return "Rarity: Rare\nName\n"..base.."\n"..s + end ++ local function lines(modLines) ++ local out = { } ++ for index, modLine in ipairs(modLines) do ++ out[index] = modLine.line ++ end ++ return out ++ end + + it("parses to craft", function() + local item = new("Item", raw([[ diff --git a/src/Classes/Item.lua b/src/Classes/Item.lua index e4998d962e..f8dbcde067 100644 --- a/src/Classes/Item.lua +++ b/src/Classes/Item.lua @@ -718,6 +718,9 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) end end end + if fullModName:match("^Allocated Crucible Passive Skill") then + linePrefix = linePrefix .. "{crucible}" + end if modTags and modTags ~= "" then linePrefix = linePrefix .. "{tags:" .. modTags:lower():gsub("%s+", "") .. "}" end @@ -859,18 +862,24 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) elseif specName == "Implicit" then self.implicit = true elseif specName == "Prefix" then - local range, affix = specVal:match("{range:([%d.]+)}(.+)") + local fractured = specVal:match("^{fractured}") and true + specVal = specVal:gsub("^{fractured}", "") + local range, affix = specVal:match("{range:([^}]+)}(.+)") range = range or ((affix or specVal) ~= "None" and main.defaultItemAffixQuality) t_insert(self.prefixes, { modId = affix or specVal, range = tonumber(range), + fractured = fractured, }) elseif specName == "Suffix" then - local range, affix = specVal:match("{range:([%d.]+)}(.+)") + local fractured = specVal:match("^{fractured}") and true + specVal = specVal:gsub("^{fractured}", "") + local range, affix = specVal:match("{range:([^}]+)}(.+)") range = range or ((affix or specVal) ~= "None" and main.defaultItemAffixQuality) t_insert(self.suffixes, { modId = affix or specVal, range = tonumber(range), + fractured = fractured, }) elseif specName == "Implicits" then implicitLines = specToNumber(specVal) or 0 @@ -1105,9 +1114,14 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) -- Use rolling Delta/Range in case one range is 1-3 and another is 1-100 so we get the finest precision possible local bestPrecisionDelta = -1 local bestPrecisionRange = -1 + local affixMod = self.affixes[self.pendingAffixList[1].modId] + modLine.order = affixMod and affixMod.statOrder[1] for value, range in line:gmatch("(%-?%d+%.?%d*)%((%-?%d+%.?%d*%-%-?%d+%.?%d*)%)") do -- Find advanced copy paste format: 45(40-50) local min, max = range:match("(%-?%d+%.?%d*)%-(%-?%d+%.?%d*)") + if tonumber(min) > tonumber(max) then + min, max = max, min + end local delta = tonumber(max) - min line = line:gsub(value .. "%(" .. range:gsub("%-", "%%-") .. "%)", value) if delta > bestPrecisionDelta then @@ -1117,7 +1131,10 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) end t_insert(self.pendingAffixList[1].table, { modId = self.pendingAffixList[1].modId, - range = bestPrecisionRange >= 0 and bestPrecisionRange <= 1 and bestPrecisionRange or 0.5, + -- Legacy modifiers can roll outside the current data range. Keep the + -- extrapolated range so crafting a different affix doesn't normalise it. + range = bestPrecisionDelta > 0 and bestPrecisionRange or 0.5, + fractured = modLine.fractured, }) self.pendingAffixList = {} else @@ -1135,16 +1152,18 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) local min, max = range:match("(%-?%d+%.?%d*)%-(%-?%d+%.?%d*)") local delta = tonumber(max) - min if delta > bestPrecisionDelta then - bestPrecisionRange = round((value - min) / delta, 3) + bestPrecisionRange = round((value - min) / delta, 6) bestPrecisionDelta = delta end if bestPrecisionRange > 1 or bestPrecisionRange < 0 then line = line:gsub(value .. "%(" .. range:gsub("%-", "%%-") .. "%)", value) else - line = line:gsub(value .. "%(" .. range:gsub("%-", "%%-") .. "%)", (tonumber(value) < 0 and "+" or "") .. "(" .. range .. ")") + line = line:gsub(value .. "%(" .. range:gsub("%-", "%%-") .. "%)", (tonumber(value) < 0 and "+" or "") .. "(" .. min .. "-" .. max .. ")") end end - if bestPrecisionRange <= 1 and bestPrecisionRange >= 0 then + if hasIndependentRolls then + line = advancedCopyLine:gsub("(%-?%d+%.?%d*)%(%-?%d+%.?%d*%-%-?%d+%.?%d*%)", "%1") + elseif bestPrecisionRange <= 1 and bestPrecisionRange >= 0 then modLine.range = bestPrecisionRange end end @@ -1770,11 +1789,13 @@ function ItemClass:BuildRaw() 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) + for _, affix in ipairs(self.prefixes or { }) do + local range = affix.range and "{range:" .. (type(affix.range) == "table" and table.concat(affix.range, ",") or round(affix.range, 3)) .. "}" or "" + t_insert(rawLines, "Prefix: " .. (affix.fractured and "{fractured}" or "") .. range .. 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) + for _, affix in ipairs(self.suffixes or { }) do + local range = affix.range and "{range:" .. (type(affix.range) == "table" and table.concat(affix.range, ",") or round(affix.range, 3)) .. "}" or "" + t_insert(rawLines, "Suffix: " .. (affix.fractured and "{fractured}" or "") .. range .. affix.modId) end end if self.catalyst and self.catalyst > 0 then @@ -2147,6 +2168,9 @@ function ItemClass:Craft() for _, mod in ipairs(savedMods) do t_insert(self.explicitModLines, mod) end + if #self.explicitModLines > 1 then + sortCraftedModLines(self.explicitModLines) + end self:BuildAndParseRaw() end diff --git a/src/Classes/Item.lua.rej b/src/Classes/Item.lua.rej new file mode 100644 index 0000000000..a03dcf6c5d --- /dev/null +++ b/src/Classes/Item.lua.rej @@ -0,0 +1,112 @@ +diff a/src/Classes/Item.lua b/src/Classes/Item.lua (rejected hunks) +@@ -685,24 +685,26 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) + self.crucible = true + elseif specName == "Implicit" then + self.implicit = true +- elseif specName == "Prefix" then ++ elseif specName == "Prefix" or specName == "Suffix" then ++ local affixes = specName == "Prefix" and self.prefixes or self.suffixes + local fractured = specVal:match("^{fractured}") and true + specVal = specVal:gsub("^{fractured}", "") + local range, affix = specVal:match("{range:([^}]+)}(.+)") +- range = range or ((affix or specVal) ~= "None" and main.defaultItemAffixQuality) +- t_insert(self.prefixes, { +- modId = affix or specVal, +- range = tonumber(range), +- fractured = fractured, +- }) +- elseif specName == "Suffix" then +- local fractured = specVal:match("^{fractured}") and true +- specVal = specVal:gsub("^{fractured}", "") +- local range, affix = specVal:match("{range:([^}]+)}(.+)") +- range = range or ((affix or specVal) ~= "None" and main.defaultItemAffixQuality) +- t_insert(self.suffixes, { ++ if range and range:find(",", 1, true) then ++ local ranges = { } ++ for value in range:gmatch("[^,]+") do ++ t_insert(ranges, tonumber(value)) ++ end ++ range = ranges ++ else ++ range = tonumber(range) ++ end ++ if not range and (affix or specVal) ~= "None" then ++ range = main.defaultItemAffixQuality ++ end ++ t_insert(affixes, { + modId = affix or specVal, +- range = tonumber(range), ++ range = range, + fractured = fractured, + }) + elseif specName == "Implicits" then +@@ -916,34 +918,40 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) + if #self.pendingAffixList > 1 then + -- Probably a conqueror or essence mod since the mod name is the same for all of them + -- Try to match the line against one of the mods there +- local valueStrippedLine = line:gsub("%-?%d+%.?%d*%(", "("):gsub("%-?%d+%.?%d*", "#") ++ local rangeLine = line:gsub("%-?%d+%.?%d*%(", "(") ++ local valueStrippedLine = rangeLine:gsub("%-?%d+%.?%d*", "#") ++ local exactAffix ++ local fallbackAffix + for _, pendingAffix in ipairs(self.pendingAffixList) do + local modData = self.affixes[pendingAffix.modId] + for _, modDataLine in ipairs(modData) do +- -- Prefer the exact match +- if line == modDataLine then +- self.pendingAffixList = { pendingAffix } ++ if line == modDataLine or rangeLine == modDataLine then ++ exactAffix = pendingAffix + break + end +- if valueStrippedLine == modDataLine:gsub("%-?%d+%.?%d*", "#") then +- self.pendingAffixList = { pendingAffix } +- break ++ if not fallbackAffix and valueStrippedLine == modDataLine:gsub("%-?%d+%.?%d*", "#") then ++ fallbackAffix = pendingAffix + end +- end ++ end ++ if exactAffix then ++ break ++ end + end ++ self.pendingAffixList = { exactAffix or fallbackAffix or self.pendingAffixList[1] } + end + -- Use rolling Delta/Range in case one range is 1-3 and another is 1-100 so we get the finest precision possible + local bestPrecisionDelta = -1 + local bestPrecisionRange = -1 ++ local rollRanges = { } + local affixMod = self.affixes[self.pendingAffixList[1].modId] + modLine.order = affixMod and affixMod.statOrder[1] + for value, range in line:gmatch("(%-?%d+%.?%d*)%((%-?%d+%.?%d*%-%-?%d+%.?%d*)%)") do +- -- Find advanced copy paste format: 45(40-50) + local min, max = range:match("(%-?%d+%.?%d*)%-(%-?%d+%.?%d*)") + if tonumber(min) > tonumber(max) then + min, max = max, min + end + local delta = tonumber(max) - min ++ t_insert(rollRanges, delta > 0 and round((value - min) / delta, 6) or 0.5) + line = line:gsub(value .. "%(" .. range:gsub("%-", "%%-") .. "%)", value) + if delta > bestPrecisionDelta then + bestPrecisionRange = round((value - min) / delta, 3) +@@ -954,17 +962,16 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) + modId = self.pendingAffixList[1].modId, + -- Legacy modifiers can roll outside the current data range. Keep the + -- extrapolated range so crafting a different affix doesn't normalise it. +- range = bestPrecisionDelta > 0 and bestPrecisionRange or 0.5, ++ range = #rollRanges > 1 and rollRanges or bestPrecisionDelta > 0 and bestPrecisionRange or 0.5, + fractured = modLine.fractured, + }) + self.pendingAffixList = {} + else +- -- Use rolling Delta/Range in case one range is 1-3 and another is 1-100 so we get the finest precision possible + local bestPrecisionDelta = -1 + local bestPrecisionRange = -1 + local firstRollRange + local hasIndependentRolls +- ++ + -- Advanced copy only provides the endpoints for enum ranges; keep the selected value. + line = line:gsub("(%s*)(%b())", function(space, range) + if range:find("-", 1, true) and not range:find("%d") then diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index d355d22742..50399b9365 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -2332,7 +2332,10 @@ function ItemsTabClass:UpdateAffixControl(control, item, type, outputTable, outp if control.list[control.selIndex].haveRange then control.slider.divCount = #control.list[control.selIndex].modList local index = isValueInArray(control.list[control.selIndex].modList, selAffix) - local range = item[outputTable][outputIndex].range or 0.5 + -- Imported legacy rolls can sit outside the current 0-1 affix range. + -- Keep that value on the affix, but show the nearest slider endpoint. + local affixRange = item[outputTable][outputIndex].range + local range = m_min(1, m_max(0, type(affixRange) == "table" and affixRange[1] or affixRange or 0.5)) -- Avoid exact integer boundary that slider:GetDivVal's ceil would assign to the previous segment if range == 0 and index > 1 then range = 1e-4 @@ -3802,6 +3805,11 @@ function ItemsTabClass:AddItemTooltip(tooltip, item, slot, dbMode, maxWidth) end tooltip:AddSeparator(10) + if item.memoryStrands then + tooltip:AddLine(fontSizeBig, colorCodes.CRAFTED.."Memory Strands: ^7"..item.memoryStrands, "FONTIN SC") + tooltip:AddSeparator(10) + end + if item.talismanTier then tooltip:AddLine(fontSizeBig, "^x7F7F7FTalisman Tier ^xFFFFFF"..item.talismanTier, "FONTIN SC") tooltip:AddSeparator(10) diff --git a/src/Classes/ItemsTab.lua.rej b/src/Classes/ItemsTab.lua.rej new file mode 100644 index 0000000000..a65df75e19 --- /dev/null +++ b/src/Classes/ItemsTab.lua.rej @@ -0,0 +1,19 @@ +diff a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua (rejected hunks) +@@ -1944,7 +1944,7 @@ function ItemsTabClass:UpdateAffixControls() + self:UpdateCustomControls() + end + +-function ItemsTabClass:UpdateAffixControl(control, item, type, outputTable, outputIndex) ++function ItemsTabClass:UpdateAffixControl(control, item, affixType, outputTable, outputIndex) + local extraTags = { } + local excludeGroups = { } + for _, table in ipairs({"prefixes","suffixes"}) do +@@ -1974,7 +1974,7 @@ function ItemsTabClass:UpdateAffixControl(control, item, type, outputTable, outp + local affixList = { } + local retainedAffixes = { } + for modId, mod in pairs(item.affixes) do +- if mod.type == type and not excludeGroups[mod.group] and not item:CheckIfModIsDelve(mod) then ++ if mod.type == affixType and not excludeGroups[mod.group] and not item:CheckIfModIsDelve(mod) then + if item:GetModSpawnWeight(mod, extraTags) > 0 then + t_insert(affixList, modId) + elseif modId == selAffix then diff --git a/src/Data/ModCache.lua.rej b/src/Data/ModCache.lua.rej new file mode 100644 index 0000000000..b002e8960a --- /dev/null +++ b/src/Data/ModCache.lua.rej @@ -0,0 +1,9 @@ +diff a/src/Data/ModCache.lua b/src/Data/ModCache.lua (rejected hunks) +@@ -1341,6 +1341,7 @@ c["+33% to Cold Resistance"]={{[1]={flags=0,keywordFlags=0,name="ColdResist",typ + c["+33% to Critical Strike Multiplier if Dexterity is higher than Intelligence"]={{[1]={[1]={type="Condition",var="DexHigherThanInt"},flags=0,keywordFlags=0,name="CritMultiplier",type="BASE",value=33}},nil} + c["+33% to Fire Resistance"]={{[1]={flags=0,keywordFlags=0,name="FireResist",type="BASE",value=33}},nil} + c["+33% to Global Critical Strike Multiplier"]={{[1]={[1]={type="Global"},flags=0,keywordFlags=0,name="CritMultiplier",type="BASE",value=33}},nil} ++c["+33% to Lightning Resistance"]={{[1]={flags=0,keywordFlags=0,name="LightningResist",type="BASE",value=33}},nil} + c["+33% to all Elemental Resistances"]={{[1]={flags=0,keywordFlags=0,name="ElementalResist",type="BASE",value=33}},nil} + c["+330 to Accuracy Rating"]={{[1]={flags=0,keywordFlags=0,name="Accuracy",type="BASE",value=330}},nil} + c["+333 to Accuracy Rating"]={{[1]={flags=0,keywordFlags=0,name="Accuracy",type="BASE",value=333}},nil} diff --git a/src/Modules/ItemTools.lua.rej b/src/Modules/ItemTools.lua.rej new file mode 100644 index 0000000000..e6ae55b807 --- /dev/null +++ b/src/Modules/ItemTools.lua.rej @@ -0,0 +1,15 @@ +diff a/src/Modules/ItemTools.lua b/src/Modules/ItemTools.lua (rejected hunks) +@@ -92,8 +92,12 @@ end + function itemLib.applyRange(line, range, valueScalar, baseValueScalar) + -- stripLines down to # in place of any number and store numbers inside values also remove all + signs are kept if value is positive + local values = {} ++ local rangeIndex = 0 ++ local ranges = type(range) == "table" and range + local strippedLine = line:gsub("([%+-]?)%((%-?%d+%.?%d*)%-(%-?%d+%.?%d*)%)", function(sign, min, max) +- local value = min + range * (tonumber(max) - min) ++ rangeIndex = rangeIndex + 1 ++ local valueRange = ranges and (ranges[rangeIndex] or 0.5) or range ++ local value = min + valueRange * (tonumber(max) - min) + if sign == "-" then value = value * -1 end + return (sign == "+" and value > 0) and sign .. tostring(value) or tostring(value) + end) From 6a07724ebaeb5c48b41748979a6bca230ed8c1b2 Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Mon, 31 Aug 2026 03:22:40 +1000 Subject: [PATCH 2/3] Fix port --- spec/System/TestItemParse_spec.lua | 304 +++++-------------------- spec/System/TestItemParse_spec.lua.rej | 15 -- src/Classes/Item.lua | 143 +++++++++--- src/Classes/Item.lua.rej | 112 --------- src/Classes/ItemsTab.lua | 13 +- src/Classes/ItemsTab.lua.rej | 19 -- src/Data/ModCache.lua.rej | 9 - src/Modules/ItemTools.lua | 8 +- src/Modules/ItemTools.lua.rej | 15 -- 9 files changed, 178 insertions(+), 460 deletions(-) delete mode 100644 spec/System/TestItemParse_spec.lua.rej delete mode 100644 src/Classes/Item.lua.rej delete mode 100644 src/Classes/ItemsTab.lua.rej delete mode 100644 src/Data/ModCache.lua.rej delete mode 100644 src/Modules/ItemTools.lua.rej diff --git a/spec/System/TestItemParse_spec.lua b/spec/System/TestItemParse_spec.lua index 50328b1021..1e535cbc80 100644 --- a/spec/System/TestItemParse_spec.lua +++ b/spec/System/TestItemParse_spec.lua @@ -1107,7 +1107,7 @@ describe("TestAdvancedItemParse #item", function() ]], "Ancestral Tiara")) assert.are.equals("LocalIncreasedEnergyShieldAndLife4", item.prefixes[1].modId) assert.are.equals(0, item.prefixes[1].range) - assert.are.equals(0.833, item.explicitModLines[2].range) + assert.are.equals(0.833333, item.explicitModLines[2].range) end) it("resets linePrefix", function() @@ -1263,229 +1263,90 @@ describe("TestAdvancedItemParse #item", function() ]]) end) - it("parses allocated Crucible passive skills from advanced copy", function() - local item = new("Item", raw([[ - { Allocated Crucible Passive Skill (Tier: 1) } - -3% to Critical Strike Chance - +100% to Global Critical Strike Multiplier - { Allocated Crucible Passive Skill (Tier: 1) } - Rampage - (You gain Rampage bonuses for Killing multiple Enemies in quick succession) - ]], "Citadel Bow")) - - assert.are.equals(3, #item.crucibleModLines) - assert.are.equals(0, #item.explicitModLines) - assert.are.same({ - "-3% to Critical Strike Chance", - "+100% to Global Critical Strike Multiplier", - "Rampage", - }, lines(item.crucibleModLines)) - end) - - it("ignores attribute requirements from socketed gems", function() - local item = new("Item", raw([[ - Requirements: - Str: 126 (unmet) - Dex: 185 (unmet) - Int: 129 (unmet) - -------- - Sockets: W-W-W-W-W-W - -------- - Item Level: 83 - ]], "Citadel Bow")) - - assert.are.same({ str = 0, dex = 185, int = 0 }, { - str = item.requirements.strMod, - dex = item.requirements.dexMod, - int = item.requirements.intMod, - }) - end) - - it("orders fractured mods first and crafted mods last", function() - local item = new("Item", raw([[ - Item Level: 83 - { Fractured Prefix Modifier "Cheetah's" (Tier: 2) — Speed } - 30% increased Movement Speed - { Prefix Modifier "Athlete's" (Tier: 1) — Life } - +128(115-129) to maximum Life - { Master Crafted Prefix Modifier "Upgraded" (Rank: 2) — Mana } - +43(35-44) to maximum Mana - { Suffix Modifier "of the Jaguar" (Tier: 3) — Attribute } - +41(38-42) to Dexterity - ]], "Dragonscale Boots")) - local expectedLines = { - "30% increased Movement Speed", - "+41 to Dexterity", - "+128 to maximum Life", - "+(35-44) to maximum Mana", - } - assert.are.same(expectedLines, lines(item.explicitModLines)) - - item:Craft() - item:Craft() - assert.are.same(expectedLines, lines(item.explicitModLines)) - end) - - it("matches same-name affixes using their advanced-copy ranges", function() - local item = new("Item", raw([[ - Item Level: 85 - { Fractured Prefix Modifier "Essences" — Damage, Elemental, Fire, Attack } - Adds 100(80-109) to 179(162-189) Fire Damage - { Prefix Modifier "Essences" — Damage, Elemental, Lightning, Attack } - Adds 14(13-19) to 285(266-310) Lightning Damage - ]], "Kinetic Wand")) - - assert.are.equals("LocalAddedFireDamageEssence7", item.prefixes[1].modId) - assert.are.equals("LocalAddedLightningDamageEssence7_", item.prefixes[2].modId) + it("preserves independently rolled affix values when crafting", function() + local item = new("Item"):Item(raw([[ + { Fractured Prefix Modifier "Frigid" (Tier: 4) — Damage, Elemental, Cold, Attack } + Adds 7(7-8) to 14(12-14) Cold damage to Attacks + ]], "Refined Bracers")) + assert.are.equals("AddedColdDamage4", item.prefixes[1].modId) + assert.are.same({ 0, 1 }, item.prefixes[1].range) + assert.is_true(item.prefixes[1].fractured) item:Craft() - item:Craft() - assert.are.equals("Adds 100 to 179 Fire Damage", item.explicitModLines[1].line) - assert.are.equals("Adds 14 to 285 Lightning Damage", item.explicitModLines[2].line) + assert.are.equals("Adds 7 to 14 Cold damage to Attacks", item.explicitModLines[1].line) + assert.is_true(item.explicitModLines[1].fractured) end) - it("filters flask base properties and parses fixed-value advanced rolls", function() - local item = new("Item", [[ - Rarity: Unique - Soul Catcher - Quartz Flask - -------- - Lasts 7.20 (augmented) Seconds - Consumes 30 of 60 Charges on use - Currently has 59 Charges - +10% chance to Suppress Spell Damage - (40% of Damage from Suppressed Hits and Ailments they inflict is prevented) - Phasing - -------- + it("parses fixed values, enum ranges, and descending ranges", function() + local item = new("Item"):Item(raw([[ { Unique Modifier } - Consumes Maximum Charges to use + +40(10) to maximum Mana { Unique Modifier } - Vaal Skills used during effect have 40(10)% reduced Soul Gain Prevention Duration - ]]) + Maximum number of Companions (Minions-Totems) is Doubled + { Unique Modifier } + 8(10-5)% reduced Attribute Requirements + ]])) - assert.are.equals(2, #item.buffModLines) - assert.are.equals(0, #item.implicitModLines) - assert.are.equals(2, #item.explicitModLines) - assert.are.equals("Consumes Maximum Charges to use", item.explicitModLines[1].line) - assert.are.equals("Vaal Skills used during effect have 40% reduced Soul Gain Prevention Duration", item.explicitModLines[2].line) + assert.are.equals("+40 to maximum Mana", item.explicitModLines[1].line) + assert.are.equals("Maximum number of Companions is Doubled", item.explicitModLines[2].line) + assert.are.equals("(5-10)% reduced Attribute Requirements", item.explicitModLines[3].line) + assert.are.equals("8% reduced Attribute Requirements", + itemLib.applyRange(item.explicitModLines[3].line, item.explicitModLines[3].range)) end) - it("preserves rolls from large advanced-copy ranges", function() - local item = new("Item", [[ - Rarity: Unique - Elegant Hubris - Timeless Jewel + it("preserves precision for large advanced-copy ranges", function() + local item = new("Item"):Item(raw([[ { Unique Modifier } - Commissioned 150720(2000-160000) coins to commemorate Chitus(Cadiro-Victario) - ]]) - - local seedLine = itemLib.applyRange(item.explicitModLines[1].line, item.explicitModLines[1].range) - assert.are.equals("Commissioned 150720 coins to commemorate Chitus", seedLine) + Commissioned 150720(2000-160000) coins + ]])) + assert.are.equals("Commissioned 150720 coins", + itemLib.applyRange(item.explicitModLines[1].line, item.explicitModLines[1].range)) item:BuildAndParseRaw() - seedLine = itemLib.applyRange(item.explicitModLines[1].line, item.explicitModLines[1].range) - assert.are.equals("Commissioned 150720 coins to commemorate Chitus", seedLine) + assert.are.equals("Commissioned 150720 coins", + itemLib.applyRange(item.explicitModLines[1].line, item.explicitModLines[1].range)) end) - it("preserves independently rolled values on the same modifier line", function() - local item = new("Item", [[ - Rarity: Unique - Prismweave - Rustic Sash - { Unique Modifier — Damage, Elemental, Fire, Attack } - Adds 16(14-16) to 32(30-32) Fire Damage to Attacks - { Unique Modifier — Damage, Elemental, Cold, Attack } - Adds 10(10-12) to 27(24-28) Cold Damage to Attacks - ]]) - - assert.are.equals("Adds (14-16) to (30-32) Fire Damage to Attacks", item.explicitModLines[1].line) - assert.are.equals("Adds 10 to 27 Cold Damage to Attacks", item.explicitModLines[2].line) - end) - - it("orders advanced-copy unique modifiers by their database stat order", function() - local item = new("Item", [[ + it("orders advanced-copy unique modifiers by database stat order", function() + local item = new("Item"):Item([[ Rarity: Unique - Geofri's Sanctuary - Elegant Ringmail - { Unique Modifier — Life } - +66(60-70) to maximum Life - { Unique Modifier — Defences, Energy Shield } - +31(30-40) to maximum Energy Shield - { Unique Modifier — Defences, Armour, Energy Shield } - 63(50-75)% increased Armour and Energy Shield - { Unique Modifier — Life, Defences, Energy Shield } - Zealot's Oath - { Unique Modifier — Defences, Energy Shield } - +2 maximum Energy Shield per 5 Strength - { Unique Modifier — Elemental, Resistance } - +18(14-18)% to all Elemental Resistances + Evergrasping Ring + Pearl Ring + { Implicit Modifier — Caster, Speed } + 8(7-10)% increased Cast Speed + { Unique Modifier — Chaos } + Enemies in your Presence Gain 8(6-12)% of Damage as Extra Chaos Damage + { Unique Modifier — Chaos } + Allies in your Presence Gain 22(15-25)% of Damage as Extra Chaos Damage + { Unique Modifier — Mana } + +91(60-100) to maximum Mana ]]) assert.are.same({ - "(50-75)% increased Armour and Energy Shield", - "+(30-40) to maximum Energy Shield", - "+(60-70) to maximum Life", - "+(14-18)% to all Elemental Resistances", - "+2 maximum Energy Shield per 5 Strength", - "Zealot's Oath", - }, lines(item.explicitModLines)) - end) - - it("keeps the selected value from advanced-copy enum ranges", function() - local item = new("Item", [[ - Rarity: Unique - The Dark Monarch - Lich's Circlet - { Unique Modifier } - Maximum number of Raised Zombies (Animated Weapons-Holy Armaments) is Doubled - Cannot have Minions other than Raised Zombies (Animated Weapons-Holy Armaments) - ]]) - - assert.are.equals("Maximum number of Raised Zombies is Doubled", item.explicitModLines[1].line) - assert.are.equals("Cannot have Minions other than Raised Zombies", item.explicitModLines[2].line) - assert.is_true(#item.explicitModLines[1].modList > 0) + "+(60-100) to maximum Mana", + "Allies in your Presence Gain (15-25)% of Damage as Extra Chaos Damage", + "Enemies in your Presence Gain (6-12)% of Damage as Extra Chaos Damage", + }, { + item.explicitModLines[1].line, + item.explicitModLines[2].line, + item.explicitModLines[3].line, + }) end) - it("parses punctuated enum and descending numeric ranges", function() - local gemItem = new("Item", [[ - Rarity: Unique - Replica Dragonfang's Flight - Onyx Amulet - { Unique Modifier } - +3 to Level of all Lightning Tendrils(Fireball-Mana-Infused Staff) Gems - ]]) - assert.are.equals("+3 to Level of all Lightning Tendrils Gems", gemItem.explicitModLines[1].line) - assert.is_true(#gemItem.explicitModLines[1].modList > 0) - - local requirementItem = new("Item", [[ + it("filters flask state and base-property lines", function() + local item = new("Item"):Item([[ Rarity: Unique - Replica Dragonfang's Flight - Onyx Amulet + Blood of the Warrior + Gargantuan Life Flask + Lasts 7.20 (augmented) Seconds + Consumes 30 of 60 Charges on use + Currently has 59 Charges { Unique Modifier } - Items and Gems have 8(10-5)% reduced Attribute Requirements - ]]) - assert.are.equals("Items and Gems have (5-10)% reduced Attribute Requirements", requirementItem.explicitModLines[1].line) - assert.are.equals("Items and Gems have 8% reduced Attribute Requirements", - itemLib.applyRange(requirementItem.explicitModLines[1].line, requirementItem.explicitModLines[1].range)) - end) - - it("parses Memory Strands as an item property", function() - local item = new("Item", [[ - Rarity: Magic - Imperial Maul of Revitalization - Weapon Range: 1.3 metres - Memory Strands: 70 - Item Level: 85 - { Suffix Modifier "of Revitalization" (Tier: 1) — Life, Attack } - Grants 28(27-30) Life per Enemy Hit + 90% less Life Recovered ]]) - assert.are.equals(70, item.memoryStrands) - assert.are.equals(1, #item.explicitModLines) - - item:BuildAndParseRaw() - assert.are.equals(70, item.memoryStrands) assert.are.equals(1, #item.explicitModLines) + assert.are.equals("90% less Life Recovered", item.explicitModLines[1].line) end) describe("mod magnitude scaling", function() @@ -1509,51 +1370,6 @@ describe("TestAdvancedItemParse #item", function() return build.calcsTab.mainEnv.modDB:Sum("INC", { flags = ModFlag.Spell }, "Damage") end - it("scales advanced-copy Simplex Amulet explicit mods on the first parse", function() - local rawItem = [[ - Rarity: Rare - Grim Collar - Simplex Amulet - Quality (Critical Modifiers): +20% (augmented) - { Implicit Modifier } - -2 Prefix Modifiers allowed - -1 Suffix Modifier allowed - 100% increased Explicit Modifier magnitudes - { Prefix Modifier "The Elder's" (Tier: 1) — Damage, Chaos — 100% Increased } - Gain 13(3-5)% of Non-Chaos Damage as extra Chaos Damage - { Suffix Modifier "of Destruction" (Tier: 1) — Damage, Critical — 120% Increased } - +70(35-38)% to Global Critical Strike Multiplier - { Suffix Modifier "of Amassment" — Drop — 100% Increased } - 20(17-20)% increased Quantity of Items found - Shaper Item - Elder Item - ]] - - build.itemsTab:CreateDisplayItemFromRaw(rawItem, true) - local firstItem = build.itemsTab.displayItem - local function findModLine(line) - for _, modLine in ipairs(firstItem.explicitModLines) do - if modLine.line == line then - return modLine - end - end - end - assert.are.equals(2, findModLine("Gain 13% of Non-Chaos Damage as extra Chaos Damage").valueScalar) - assert.are.equals(2.2, findModLine("+70% to Global Critical Strike Multiplier").valueScalar) - - -- Changing a different affix must not normalise either legacy roll. - firstItem.suffixes[2].range = 0 - firstItem:Craft() - assert.are.equals(2, findModLine("Gain 13% of Non-Chaos Damage as extra Chaos Damage").valueScalar) - assert.are.equals(2.2, findModLine("+70% to Global Critical Strike Multiplier").valueScalar) - - -- Editing the legacy affix itself deliberately returns it to the current range. - firstItem.suffixes[1].range = 1 - firstItem:Craft() - assert.are.equals(2, findModLine("Gain 13% of Non-Chaos Damage as extra Chaos Damage").valueScalar) - assert.are.equals(2.2, findModLine("+38% to Global Critical Strike Multiplier").valueScalar) - end) - it("scales matching implicit mods by modifier magnitude", function() -- 130% * 1.7 = 221 build.itemsTab:CreateDisplayItemFromRaw([[ diff --git a/spec/System/TestItemParse_spec.lua.rej b/spec/System/TestItemParse_spec.lua.rej deleted file mode 100644 index cb3954cf53..0000000000 --- a/spec/System/TestItemParse_spec.lua.rej +++ /dev/null @@ -1,15 +0,0 @@ -diff a/spec/System/TestItemParse_spec.lua b/spec/System/TestItemParse_spec.lua (rejected hunks) -@@ -506,6 +506,13 @@ describe("TestAdvancedItemParse #item", function() - base = base or "Plate Vest" - return "Rarity: Rare\nName\n"..base.."\n"..s - end -+ local function lines(modLines) -+ local out = { } -+ for index, modLine in ipairs(modLines) do -+ out[index] = modLine.line -+ end -+ return out -+ end - - it("parses to craft", function() - local item = new("Item", raw([[ diff --git a/src/Classes/Item.lua b/src/Classes/Item.lua index f8dbcde067..d343106920 100644 --- a/src/Classes/Item.lua +++ b/src/Classes/Item.lua @@ -62,6 +62,31 @@ local function getCatalystScalar(catalystId, mod, quality) return 1 end +local function normaliseModLine(line) + return line:gsub("%d+%.?%d*", "#") + :gsub("%(%-?#%-#%)", "#"):lower() + :gsub("\n", " ") +end + +local uniqueModStatOrder + +local function sortCraftedModLines(modLines) + local sourceOrder = { } + for index, modLine in ipairs(modLines) do + sourceOrder[modLine] = index + end + table.sort(modLines, function(a, b) + local aGroup = (a.crafted or a.custom) and 3 or a.fractured and 1 or 2 + local bGroup = (b.crafted or b.custom) and 3 or b.fractured and 1 or 2 + if aGroup ~= bGroup then + return aGroup < bGroup + elseif aGroup < 3 and a.order ~= b.order then + return (a.order or math.huge) < (b.order or math.huge) + end + return sourceOrder[a] < sourceOrder[b] + end) +end + ---@class Item local ItemClass = newClass("Item") @@ -655,11 +680,17 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) self.desecrated = true elseif line == "Requirements:" then -- nothing to do - elseif line:match("^%(%a+") then + elseif line:match("^%(%a+") or line:match("^%(%d+%%? of ") then -- Reminder text, nothing to parse while self.rawLines[l] and not self.rawLines[l]:match("%)$") do l = l + 1 end + elseif self.base and self.base.flask and ( + line:match("^Lasts .+ Seconds$") + or line:match("^Consumes %d+ of %d+ Charges on use$") + or line:match("^Currently has %d+ Charges$") + ) then + -- In-game flask state and base properties aren't modifier lines. elseif line:match("^{ ") then -- We're parsing advanced copy/paste format self.advancedCopy = true @@ -718,9 +749,6 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) end end end - if fullModName:match("^Allocated Crucible Passive Skill") then - linePrefix = linePrefix .. "{crucible}" - end if modTags and modTags ~= "" then linePrefix = linePrefix .. "{tags:" .. modTags:lower():gsub("%s+", "") .. "}" end @@ -861,24 +889,26 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) self.crafted = true elseif specName == "Implicit" then self.implicit = true - elseif specName == "Prefix" then + elseif specName == "Prefix" or specName == "Suffix" then + local affixes = specName == "Prefix" and self.prefixes or self.suffixes local fractured = specVal:match("^{fractured}") and true specVal = specVal:gsub("^{fractured}", "") local range, affix = specVal:match("{range:([^}]+)}(.+)") - range = range or ((affix or specVal) ~= "None" and main.defaultItemAffixQuality) - t_insert(self.prefixes, { - modId = affix or specVal, - range = tonumber(range), - fractured = fractured, - }) - elseif specName == "Suffix" then - local fractured = specVal:match("^{fractured}") and true - specVal = specVal:gsub("^{fractured}", "") - local range, affix = specVal:match("{range:([^}]+)}(.+)") - range = range or ((affix or specVal) ~= "None" and main.defaultItemAffixQuality) - t_insert(self.suffixes, { + if range and range:find(",", 1, true) then + local ranges = { } + for value in range:gmatch("[^,]+") do + t_insert(ranges, tonumber(value)) + end + range = ranges + else + range = tonumber(range) + end + if not range and (affix or specVal) ~= "None" then + range = main.defaultItemAffixQuality + end + t_insert(affixes, { modId = affix or specVal, - range = tonumber(range), + range = range, fractured = fractured, }) elseif specName == "Implicits" then @@ -1091,38 +1121,47 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) else catalystScalar = getCatalystScalar(self.catalyst, modLine, self.catalystQuality) end + -- Advanced copy uses current(base) for fixed-value modifiers, + -- in addition to the current(min-max) form handled below. + line = line:gsub("(%-?%d+%.?%d*)%((%-?%d+%.?%d*)%)", "%1") if self.pendingAffixList and #self.pendingAffixList > 0 then if #self.pendingAffixList > 1 then -- Probably a conqueror or Essence mod since the mod name is the same for all of them -- Try to match the line against one of the mods there - local valueStrippedLine = line:gsub("%-?%d+%.?%d*%(", "("):gsub("%-?%d+%.?%d*", "#") + local rangeLine = line:gsub("%-?%d+%.?%d*%(", "(") + local valueStrippedLine = rangeLine:gsub("%-?%d+%.?%d*", "#") + local exactAffix + local fallbackAffix for _, pendingAffix in ipairs(self.pendingAffixList) do local modData = self.affixes[pendingAffix.modId] for _, modDataLine in ipairs(modData) do - -- Prefer the exact match - if line == modDataLine then - self.pendingAffixList = { pendingAffix } + if line == modDataLine or rangeLine == modDataLine then + exactAffix = pendingAffix break end - if valueStrippedLine == modDataLine:gsub("%-?%d+%.?%d*", "#") then - self.pendingAffixList = { pendingAffix } - break + if not fallbackAffix and valueStrippedLine == modDataLine:gsub("%-?%d+%.?%d*", "#") then + fallbackAffix = pendingAffix end end + if exactAffix then + break + end end + self.pendingAffixList = { exactAffix or fallbackAffix or self.pendingAffixList[1] } end -- Use rolling Delta/Range in case one range is 1-3 and another is 1-100 so we get the finest precision possible local bestPrecisionDelta = -1 local bestPrecisionRange = -1 + local rollRanges = { } local affixMod = self.affixes[self.pendingAffixList[1].modId] modLine.order = affixMod and affixMod.statOrder[1] for value, range in line:gmatch("(%-?%d+%.?%d*)%((%-?%d+%.?%d*%-%-?%d+%.?%d*)%)") do - -- Find advanced copy paste format: 45(40-50) local min, max = range:match("(%-?%d+%.?%d*)%-(%-?%d+%.?%d*)") if tonumber(min) > tonumber(max) then min, max = max, min end local delta = tonumber(max) - min + t_insert(rollRanges, delta > 0 and round((value - min) / delta, 6) or 0.5) line = line:gsub(value .. "%(" .. range:gsub("%-", "%%-") .. "%)", value) if delta > bestPrecisionDelta then bestPrecisionRange = round((value - min) / delta, 3) @@ -1133,7 +1172,7 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) modId = self.pendingAffixList[1].modId, -- Legacy modifiers can roll outside the current data range. Keep the -- extrapolated range so crafting a different affix doesn't normalise it. - range = bestPrecisionDelta > 0 and bestPrecisionRange or 0.5, + range = #rollRanges > 1 and rollRanges or bestPrecisionDelta > 0 and bestPrecisionRange or 0.5, fractured = modLine.fractured, }) self.pendingAffixList = {} @@ -1141,18 +1180,31 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) -- Use rolling Delta/Range in case one range is 1-3 and another is 1-100 so we get the finest precision possible local bestPrecisionDelta = -1 local bestPrecisionRange = -1 + local firstRollRange + local hasIndependentRolls - -- Replace non-number ranges as unsupported - line = line:gsub("(%a+)%([%a%s]+%-[%a%s]+%)", "%1") - - -- Strip single values like 25(50) -> 25 - line = line:gsub("(%d+)%((%d+)%)", "%1") + -- Advanced copy only provides the endpoints for enum ranges; keep the selected value. + line = line:gsub("(%s*)(%b())", function(space, range) + if range:find("-", 1, true) and not range:find("%d") then + return "" + end + return space .. range + end) + local advancedCopyLine = line for value, range in line:gmatch("(%-?%d+%.?%d*)%((%-?%d+%.?%d*%-%-?%d+%.?%d*)%)") do local min, max = range:match("(%-?%d+%.?%d*)%-(%-?%d+%.?%d*)") + if tonumber(min) > tonumber(max) then + min, max = max, min + end local delta = tonumber(max) - min + local rollRange = delta > 0 and round((value - min) / delta, 6) or 0.5 + if firstRollRange and firstRollRange ~= rollRange then + hasIndependentRolls = true + end + firstRollRange = firstRollRange or rollRange if delta > bestPrecisionDelta then - bestPrecisionRange = round((value - min) / delta, 6) + bestPrecisionRange = rollRange bestPrecisionDelta = delta end if bestPrecisionRange > 1 or bestPrecisionRange < 0 then @@ -1544,6 +1596,27 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) self.runes = { } end end + if self.advancedCopy and (self.rarity == "UNIQUE" or self.rarity == "RELIC") and not self:UsesVersionedOrGroupedVariants() then + if not uniqueModStatOrder then + uniqueModStatOrder = { exact = { }, normalised = { } } + for _, mod in pairs(data.itemMods.Exclusive) do + for index, line in ipairs(mod) do + local exactLine = line:lower():gsub("\n", " ") + local statLine = normaliseModLine(line) + uniqueModStatOrder.exact[exactLine] = m_min(uniqueModStatOrder.exact[exactLine] or math.huge, mod.statOrder[index]) + uniqueModStatOrder.normalised[statLine] = m_min(uniqueModStatOrder.normalised[statLine] or math.huge, mod.statOrder[index]) + end + end + end + for _, modLine in ipairs(self.explicitModLines) do + local exactLine = modLine.line:lower():gsub("\n", " ") + modLine.order = uniqueModStatOrder.exact[exactLine] + or uniqueModStatOrder.normalised[normaliseModLine(modLine.line)] + end + end + if self.advancedCopy and #self.explicitModLines > 1 then + sortCraftedModLines(self.explicitModLines) + end if self.advancedCopy or self.crafted then -- apply mod magnitude boost to matching mods if #self.modMagnitudeMods > 0 then @@ -1837,7 +1910,7 @@ function ItemClass:BuildRaw() line = displayValueScalar and itemLib.applyRange(modLine.line, modLine.range or main.defaultItemAffixQuality, displayValueScalar, modLine.corruptedRange) or modLine.line end if modLine.range and line:match("%(%-?[%d%.]+%-%-?[%d%.]+%)") then - line = "{range:" .. round(modLine.range, 3) .. "}" .. line + line = "{range:" .. round(modLine.range, 6) .. "}" .. line end if modLine.corruptedRange then line = "{corruptedRange:" .. round(modLine.corruptedRange, 2) .. "}" .. line @@ -2149,7 +2222,7 @@ function ItemClass:Craft() return tonumber(num) + tonumber(other) end) else - local modLine = { line = line, order = order, type = mod.type, modTags = mod.modTags or { }, unscalable = mod.unscalable } + local modLine = { line = line, order = order, type = mod.type, modTags = mod.modTags or { }, unscalable = mod.unscalable, fractured = affix.fractured } modLine[mod.type:lower()] = true for l = 1, #self.explicitModLines + 1 do if not self.explicitModLines[l] or self.explicitModLines[l].order > order then diff --git a/src/Classes/Item.lua.rej b/src/Classes/Item.lua.rej deleted file mode 100644 index a03dcf6c5d..0000000000 --- a/src/Classes/Item.lua.rej +++ /dev/null @@ -1,112 +0,0 @@ -diff a/src/Classes/Item.lua b/src/Classes/Item.lua (rejected hunks) -@@ -685,24 +685,26 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) - self.crucible = true - elseif specName == "Implicit" then - self.implicit = true -- elseif specName == "Prefix" then -+ elseif specName == "Prefix" or specName == "Suffix" then -+ local affixes = specName == "Prefix" and self.prefixes or self.suffixes - local fractured = specVal:match("^{fractured}") and true - specVal = specVal:gsub("^{fractured}", "") - local range, affix = specVal:match("{range:([^}]+)}(.+)") -- range = range or ((affix or specVal) ~= "None" and main.defaultItemAffixQuality) -- t_insert(self.prefixes, { -- modId = affix or specVal, -- range = tonumber(range), -- fractured = fractured, -- }) -- elseif specName == "Suffix" then -- local fractured = specVal:match("^{fractured}") and true -- specVal = specVal:gsub("^{fractured}", "") -- local range, affix = specVal:match("{range:([^}]+)}(.+)") -- range = range or ((affix or specVal) ~= "None" and main.defaultItemAffixQuality) -- t_insert(self.suffixes, { -+ if range and range:find(",", 1, true) then -+ local ranges = { } -+ for value in range:gmatch("[^,]+") do -+ t_insert(ranges, tonumber(value)) -+ end -+ range = ranges -+ else -+ range = tonumber(range) -+ end -+ if not range and (affix or specVal) ~= "None" then -+ range = main.defaultItemAffixQuality -+ end -+ t_insert(affixes, { - modId = affix or specVal, -- range = tonumber(range), -+ range = range, - fractured = fractured, - }) - elseif specName == "Implicits" then -@@ -916,34 +918,40 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) - if #self.pendingAffixList > 1 then - -- Probably a conqueror or essence mod since the mod name is the same for all of them - -- Try to match the line against one of the mods there -- local valueStrippedLine = line:gsub("%-?%d+%.?%d*%(", "("):gsub("%-?%d+%.?%d*", "#") -+ local rangeLine = line:gsub("%-?%d+%.?%d*%(", "(") -+ local valueStrippedLine = rangeLine:gsub("%-?%d+%.?%d*", "#") -+ local exactAffix -+ local fallbackAffix - for _, pendingAffix in ipairs(self.pendingAffixList) do - local modData = self.affixes[pendingAffix.modId] - for _, modDataLine in ipairs(modData) do -- -- Prefer the exact match -- if line == modDataLine then -- self.pendingAffixList = { pendingAffix } -+ if line == modDataLine or rangeLine == modDataLine then -+ exactAffix = pendingAffix - break - end -- if valueStrippedLine == modDataLine:gsub("%-?%d+%.?%d*", "#") then -- self.pendingAffixList = { pendingAffix } -- break -+ if not fallbackAffix and valueStrippedLine == modDataLine:gsub("%-?%d+%.?%d*", "#") then -+ fallbackAffix = pendingAffix - end -- end -+ end -+ if exactAffix then -+ break -+ end - end -+ self.pendingAffixList = { exactAffix or fallbackAffix or self.pendingAffixList[1] } - end - -- Use rolling Delta/Range in case one range is 1-3 and another is 1-100 so we get the finest precision possible - local bestPrecisionDelta = -1 - local bestPrecisionRange = -1 -+ local rollRanges = { } - local affixMod = self.affixes[self.pendingAffixList[1].modId] - modLine.order = affixMod and affixMod.statOrder[1] - for value, range in line:gmatch("(%-?%d+%.?%d*)%((%-?%d+%.?%d*%-%-?%d+%.?%d*)%)") do -- -- Find advanced copy paste format: 45(40-50) - local min, max = range:match("(%-?%d+%.?%d*)%-(%-?%d+%.?%d*)") - if tonumber(min) > tonumber(max) then - min, max = max, min - end - local delta = tonumber(max) - min -+ t_insert(rollRanges, delta > 0 and round((value - min) / delta, 6) or 0.5) - line = line:gsub(value .. "%(" .. range:gsub("%-", "%%-") .. "%)", value) - if delta > bestPrecisionDelta then - bestPrecisionRange = round((value - min) / delta, 3) -@@ -954,17 +962,16 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) - modId = self.pendingAffixList[1].modId, - -- Legacy modifiers can roll outside the current data range. Keep the - -- extrapolated range so crafting a different affix doesn't normalise it. -- range = bestPrecisionDelta > 0 and bestPrecisionRange or 0.5, -+ range = #rollRanges > 1 and rollRanges or bestPrecisionDelta > 0 and bestPrecisionRange or 0.5, - fractured = modLine.fractured, - }) - self.pendingAffixList = {} - else -- -- Use rolling Delta/Range in case one range is 1-3 and another is 1-100 so we get the finest precision possible - local bestPrecisionDelta = -1 - local bestPrecisionRange = -1 - local firstRollRange - local hasIndependentRolls -- -+ - -- Advanced copy only provides the endpoints for enum ranges; keep the selected value. - line = line:gsub("(%s*)(%b())", function(space, range) - if range:find("-", 1, true) and not range:find("%d") then diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index 50399b9365..54a30006b2 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -818,7 +818,7 @@ holding Shift will put it in the second.]]) return range end drop = new("DropDownControl"):DropDownControl({ "TOPLEFT", prev, "TOPLEFT" }, { i == 1 and 40 or 0, 0, 418, 20 }, nil, function(index, value) - local affix = { modId = "None" } + local affix = { modId = "None", fractured = self.displayItem[drop.outputTable][drop.outputIndex].fractured } if value.modId then affix.modId = value.modId affix.range = slider.val @@ -2001,7 +2001,7 @@ function ItemsTabClass:SetDisplayItem(item) self.controls.displayItemSocketRuneEdit:SetText(item.itemSocketCount) self.controls.displayItemSocketJewelEdit:SetText(item.jewelSocketCount) self.controls.displayItemQualityEdit:SetText(item.quality) - self.controls.displayItemCatalyst:SetSel((item.catalyst or 0) + 1) + self.controls.displayItemCatalyst:SetSel((item.catalyst or 0) + 1, true) if item.catalystQuality then self.controls.displayItemCatalystQualityEdit:SetText(m_max(item.catalystQuality, 0)) else @@ -2196,7 +2196,7 @@ function ItemsTabClass:UpdateRuneControls() end end -function ItemsTabClass:UpdateAffixControl(control, item, type, outputTable, outputIndex, powerCache) +function ItemsTabClass:UpdateAffixControl(control, item, affixType, outputTable, outputIndex, powerCache) local extraTags = { } local excludeGroups = { } for _, table in ipairs({"prefixes","suffixes"}) do @@ -2224,7 +2224,7 @@ function ItemsTabClass:UpdateAffixControl(control, item, type, outputTable, outp end local affixList = { } for modId, mod in pairs(item.affixes) do - if mod.type == type and not excludeGroups[mod.group] and item:GetModSpawnWeight(mod, extraTags) > 0 then + if mod.type == affixType and not excludeGroups[mod.group] and item:GetModSpawnWeight(mod, extraTags) > 0 then t_insert(affixList, modId) end end @@ -3805,11 +3805,6 @@ function ItemsTabClass:AddItemTooltip(tooltip, item, slot, dbMode, maxWidth) end tooltip:AddSeparator(10) - if item.memoryStrands then - tooltip:AddLine(fontSizeBig, colorCodes.CRAFTED.."Memory Strands: ^7"..item.memoryStrands, "FONTIN SC") - tooltip:AddSeparator(10) - end - if item.talismanTier then tooltip:AddLine(fontSizeBig, "^x7F7F7FTalisman Tier ^xFFFFFF"..item.talismanTier, "FONTIN SC") tooltip:AddSeparator(10) diff --git a/src/Classes/ItemsTab.lua.rej b/src/Classes/ItemsTab.lua.rej deleted file mode 100644 index a65df75e19..0000000000 --- a/src/Classes/ItemsTab.lua.rej +++ /dev/null @@ -1,19 +0,0 @@ -diff a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua (rejected hunks) -@@ -1944,7 +1944,7 @@ function ItemsTabClass:UpdateAffixControls() - self:UpdateCustomControls() - end - --function ItemsTabClass:UpdateAffixControl(control, item, type, outputTable, outputIndex) -+function ItemsTabClass:UpdateAffixControl(control, item, affixType, outputTable, outputIndex) - local extraTags = { } - local excludeGroups = { } - for _, table in ipairs({"prefixes","suffixes"}) do -@@ -1974,7 +1974,7 @@ function ItemsTabClass:UpdateAffixControl(control, item, type, outputTable, outp - local affixList = { } - local retainedAffixes = { } - for modId, mod in pairs(item.affixes) do -- if mod.type == type and not excludeGroups[mod.group] and not item:CheckIfModIsDelve(mod) then -+ if mod.type == affixType and not excludeGroups[mod.group] and not item:CheckIfModIsDelve(mod) then - if item:GetModSpawnWeight(mod, extraTags) > 0 then - t_insert(affixList, modId) - elseif modId == selAffix then diff --git a/src/Data/ModCache.lua.rej b/src/Data/ModCache.lua.rej deleted file mode 100644 index b002e8960a..0000000000 --- a/src/Data/ModCache.lua.rej +++ /dev/null @@ -1,9 +0,0 @@ -diff a/src/Data/ModCache.lua b/src/Data/ModCache.lua (rejected hunks) -@@ -1341,6 +1341,7 @@ c["+33% to Cold Resistance"]={{[1]={flags=0,keywordFlags=0,name="ColdResist",typ - c["+33% to Critical Strike Multiplier if Dexterity is higher than Intelligence"]={{[1]={[1]={type="Condition",var="DexHigherThanInt"},flags=0,keywordFlags=0,name="CritMultiplier",type="BASE",value=33}},nil} - c["+33% to Fire Resistance"]={{[1]={flags=0,keywordFlags=0,name="FireResist",type="BASE",value=33}},nil} - c["+33% to Global Critical Strike Multiplier"]={{[1]={[1]={type="Global"},flags=0,keywordFlags=0,name="CritMultiplier",type="BASE",value=33}},nil} -+c["+33% to Lightning Resistance"]={{[1]={flags=0,keywordFlags=0,name="LightningResist",type="BASE",value=33}},nil} - c["+33% to all Elemental Resistances"]={{[1]={flags=0,keywordFlags=0,name="ElementalResist",type="BASE",value=33}},nil} - c["+330 to Accuracy Rating"]={{[1]={flags=0,keywordFlags=0,name="Accuracy",type="BASE",value=330}},nil} - c["+333 to Accuracy Rating"]={{[1]={flags=0,keywordFlags=0,name="Accuracy",type="BASE",value=333}},nil} diff --git a/src/Modules/ItemTools.lua b/src/Modules/ItemTools.lua index 9914a561b2..376c5ba500 100644 --- a/src/Modules/ItemTools.lua +++ b/src/Modules/ItemTools.lua @@ -75,14 +75,18 @@ end -- Apply range value (0 to 1) to a modifier that has a range: "(x-x)" or "(x-x) to (x-x)" ---@param line string ----@param range number +---@param range number|number[] ---@param valueScalar number? ---@param baseValueScalar number? function itemLib.applyRange(line, range, valueScalar, baseValueScalar) -- stripLines down to # in place of any number and store numbers inside values also remove all + signs are kept if value is positive local values = { } + local rangeIndex = 0 + local ranges = type(range) == "table" and range local strippedLine = line:gsub("([%+-]?)%((%-?%d+%.?%d*)%-(%-?%d+%.?%d*)%)", function(sign, min, max) - local value = min + range * (tonumber(max) - min) + rangeIndex = rangeIndex + 1 + local valueRange = ranges and (ranges[rangeIndex] or 0.5) or range + local value = min + valueRange * (tonumber(max) - min) if sign == "-" then value = value * -1 end return (sign == "+" and value > 0 ) and sign..tostring(value) or tostring(value) end) diff --git a/src/Modules/ItemTools.lua.rej b/src/Modules/ItemTools.lua.rej deleted file mode 100644 index e6ae55b807..0000000000 --- a/src/Modules/ItemTools.lua.rej +++ /dev/null @@ -1,15 +0,0 @@ -diff a/src/Modules/ItemTools.lua b/src/Modules/ItemTools.lua (rejected hunks) -@@ -92,8 +92,12 @@ end - function itemLib.applyRange(line, range, valueScalar, baseValueScalar) - -- stripLines down to # in place of any number and store numbers inside values also remove all + signs are kept if value is positive - local values = {} -+ local rangeIndex = 0 -+ local ranges = type(range) == "table" and range - local strippedLine = line:gsub("([%+-]?)%((%-?%d+%.?%d*)%-(%-?%d+%.?%d*)%)", function(sign, min, max) -- local value = min + range * (tonumber(max) - min) -+ rangeIndex = rangeIndex + 1 -+ local valueRange = ranges and (ranges[rangeIndex] or 0.5) or range -+ local value = min + valueRange * (tonumber(max) - min) - if sign == "-" then value = value * -1 end - return (sign == "+" and value > 0) and sign .. tostring(value) or tostring(value) - end) From 82dd136be299fc82d2a92f73b2235e84a2eb11be Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Mon, 31 Aug 2026 03:41:43 +1000 Subject: [PATCH 3/3] Fix poe 1 only mechanics --- spec/System/TestItemParse_spec.lua | 50 ++++++++++++++---------------- src/Classes/Item.lua | 12 ++----- 2 files changed, 27 insertions(+), 35 deletions(-) diff --git a/spec/System/TestItemParse_spec.lua b/spec/System/TestItemParse_spec.lua index 1e535cbc80..335a8e3df0 100644 --- a/spec/System/TestItemParse_spec.lua +++ b/spec/System/TestItemParse_spec.lua @@ -1277,33 +1277,31 @@ describe("TestAdvancedItemParse #item", function() assert.is_true(item.explicitModLines[1].fractured) end) - it("parses fixed values, enum ranges, and descending ranges", function() - local item = new("Item"):Item(raw([[ - { Unique Modifier } - +40(10) to maximum Mana - { Unique Modifier } - Maximum number of Companions (Minions-Totems) is Doubled + it("parses fixed advanced-copy values from a legacy Prism Guardian", function() + local item = new("Item"):Item([[ + Rarity: Unique + Prism Guardian + Sectarian Crest Shield { Unique Modifier } - 8(10-5)% reduced Attribute Requirements - ]])) + +1 to Maximum Spirit per 25(50) Maximum Life + ]]) - assert.are.equals("+40 to maximum Mana", item.explicitModLines[1].line) - assert.are.equals("Maximum number of Companions is Doubled", item.explicitModLines[2].line) - assert.are.equals("(5-10)% reduced Attribute Requirements", item.explicitModLines[3].line) - assert.are.equals("8% reduced Attribute Requirements", - itemLib.applyRange(item.explicitModLines[3].line, item.explicitModLines[3].range)) + assert.are.equals("+1 to Maximum Spirit per 25 Maximum Life", item.explicitModLines[1].line) end) - it("preserves precision for large advanced-copy ranges", function() - local item = new("Item"):Item(raw([[ + it("preserves a Heroic Tragedy seed and selected commander", function() + local item = new("Item"):Item([[ + Rarity: Unique + Heroic Tragedy + Timeless Jewel { Unique Modifier } - Commissioned 150720(2000-160000) coins - ]])) + Remembrancing 7321(100-8000) songworthy deeds by the line of Vorana(Vorana-Olroth) + ]]) - assert.are.equals("Commissioned 150720 coins", + assert.are.equals("Remembrancing 7321 songworthy deeds by the line of Vorana", itemLib.applyRange(item.explicitModLines[1].line, item.explicitModLines[1].range)) item:BuildAndParseRaw() - assert.are.equals("Commissioned 150720 coins", + assert.are.equals("Remembrancing 7321 songworthy deeds by the line of Vorana", itemLib.applyRange(item.explicitModLines[1].line, item.explicitModLines[1].range)) end) @@ -1336,17 +1334,17 @@ describe("TestAdvancedItemParse #item", function() it("filters flask state and base-property lines", function() local item = new("Item"):Item([[ Rarity: Unique - Blood of the Warrior - Gargantuan Life Flask - Lasts 7.20 (augmented) Seconds - Consumes 30 of 60 Charges on use - Currently has 59 Charges + Opportunity + Ultimate Life Flask + Recovers 2061 (augmented) Life over 4.20 Seconds + Consumes 4 (augmented) of 75 Charges on use + Currently has 0 Charges { Unique Modifier } - 90% less Life Recovered + Cannot be Used manually ]]) assert.are.equals(1, #item.explicitModLines) - assert.are.equals("90% less Life Recovered", item.explicitModLines[1].line) + assert.are.equals("Cannot be Used manually", item.explicitModLines[1].line) end) describe("mod magnitude scaling", function() diff --git a/src/Classes/Item.lua b/src/Classes/Item.lua index d343106920..76bee2cfd8 100644 --- a/src/Classes/Item.lua +++ b/src/Classes/Item.lua @@ -680,14 +680,14 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) self.desecrated = true elseif line == "Requirements:" then -- nothing to do - elseif line:match("^%(%a+") or line:match("^%(%d+%%? of ") then + elseif line:match("^%(%a+") then -- Reminder text, nothing to parse while self.rawLines[l] and not self.rawLines[l]:match("%)$") do l = l + 1 end elseif self.base and self.base.flask and ( - line:match("^Lasts .+ Seconds$") - or line:match("^Consumes %d+ of %d+ Charges on use$") + line:match("^Recovers .+ over .+ Seconds?$") + or line:match("^Consumes %d+.- of %d+.- Charges on use$") or line:match("^Currently has %d+ Charges$") ) then -- In-game flask state and base properties aren't modifier lines. @@ -1157,9 +1157,6 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) modLine.order = affixMod and affixMod.statOrder[1] for value, range in line:gmatch("(%-?%d+%.?%d*)%((%-?%d+%.?%d*%-%-?%d+%.?%d*)%)") do local min, max = range:match("(%-?%d+%.?%d*)%-(%-?%d+%.?%d*)") - if tonumber(min) > tonumber(max) then - min, max = max, min - end local delta = tonumber(max) - min t_insert(rollRanges, delta > 0 and round((value - min) / delta, 6) or 0.5) line = line:gsub(value .. "%(" .. range:gsub("%-", "%%-") .. "%)", value) @@ -1194,9 +1191,6 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) for value, range in line:gmatch("(%-?%d+%.?%d*)%((%-?%d+%.?%d*%-%-?%d+%.?%d*)%)") do local min, max = range:match("(%-?%d+%.?%d*)%-(%-?%d+%.?%d*)") - if tonumber(min) > tonumber(max) then - min, max = max, min - end local delta = tonumber(max) - min local rollRange = delta > 0 and round((value - min) / delta, 6) or 0.5 if firstRollRange and firstRollRange ~= rollRange then