diff --git a/spec/System/TestItemParse_spec.lua b/spec/System/TestItemParse_spec.lua index d337f63b51..a62c26c806 100644 --- a/spec/System/TestItemParse_spec.lua +++ b/spec/System/TestItemParse_spec.lua @@ -694,6 +694,235 @@ 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", + }, { + item.crucibleModLines[1].line, + item.crucibleModLines[2].line, + item.crucibleModLines[3].line, + }) + 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, { + item.explicitModLines[1].line, + item.explicitModLines[2].line, + item.explicitModLines[3].line, + item.explicitModLines[4].line, + }) + + item:Craft() + item:Craft() + assert.are.same(expectedLines, { + item.explicitModLines[1].line, + item.explicitModLines[2].line, + item.explicitModLines[3].line, + item.explicitModLines[4].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", + }, { + item.explicitModLines[1].line, + item.explicitModLines[2].line, + item.explicitModLines[3].line, + item.explicitModLines[4].line, + item.explicitModLines[5].line, + item.explicitModLines[6].line, + }) + 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() @@ -715,6 +944,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/src/Classes/Item.lua b/src/Classes/Item.lua index 67476cf1f8..89ce9c09fd 100644 --- a/src/Classes/Item.lua +++ b/src/Classes/Item.lua @@ -61,6 +61,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 + local influenceInfo = itemLib.influenceInfo.all local ItemClass = newClass("Item", function(self, raw, rarity, highQuality) @@ -442,11 +467,17 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) self[influenceItemMap[line]] = 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 @@ -501,6 +532,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 @@ -537,6 +571,8 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) self.uniqueID = specVal elseif specName == "Item Level" then self.itemLevel = specToNumber(specVal) + elseif specName == "Memory Strands" then + self.memoryStrands = specToNumber(specVal) elseif specName == "Requires Class" then self.classRestriction = specVal elseif specName:match("Quality %([%a%s]+ Modifiers%)") then @@ -650,18 +686,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 @@ -867,6 +909,9 @@ 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 @@ -890,9 +935,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 @@ -902,31 +952,52 @@ 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 -- 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") + -- 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, 3) + bestPrecisionRange = rollRange 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 @@ -1107,6 +1178,31 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) if self.baseName and self.title then self.name = self.title .. ", " .. self.baseName:gsub(" %(.+%)","") end + if self.advancedCopy and (self.rarity == "UNIQUE" or self.rarity == "RELIC") then + if not uniqueModStatOrder then + uniqueModStatOrder = { exact = { }, normalised = { } } + for _, mod in pairs(data.itemMods.ItemExclusive) 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.base and #self.sockets > 0 then + -- In-game requirement totals include requirements from socketed gems. + -- Derive the item's attribute requirements from its base and local mods instead. + self.requirements.str = self.base.req.str or 0 + self.requirements.dex = self.base.req.dex or 0 + self.requirements.int = self.base.req.int or 0 + end if self.base and not self.requirements.level then if importedLevelReq and #self.sockets == 0 then -- Requirements on imported items can only be trusted for items with no sockets @@ -1123,7 +1219,7 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) end end end - if self.advancedCopy then + if self.advancedCopy or self.crafted then -- apply mod magnitude boost to matching mods if #self.modMagnitudeMods > 0 then for _, modMagnitudeMod in ipairs(self.modMagnitudeMods) do @@ -1174,6 +1270,9 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) end end end + if self.advancedCopy and #self.explicitModLines > 1 then + sortCraftedModLines(self.explicitModLines) + end self.affixLimit = 0 if self.crafted then if not self.affixes then @@ -1246,11 +1345,6 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) end end if self.mutatedLines then - local function normalise(line) - return line:gsub("%d+%.?%d*", "#") - :gsub("%(%-?#%-#%)", "#"):lower() - :gsub("\n", " ") - end -- Match both sides so the same checkbox can apply or revert the transformation. for origModId, foulModId in pairs(self.mutatedLines) do local function checkMod(modId, newModId, mutated) @@ -1263,10 +1357,10 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) local matchingLines = {} local matchedLines = {} for _, line in ipairs(lines) do - local statLine = normalise(line) + local statLine = normaliseModLine(line) for _, modLine in ipairs(self.explicitModLines) do if not matchedLines[modLine] - and normalise(modLine.line:gsub("\n", " ")) == statLine + and normaliseModLine(modLine.line) == statLine and self:CheckModLineVariant(modLine) then matchedLines[modLine] = true t_insert(matchingLines, modLine) @@ -1496,10 +1590,10 @@ function ItemClass:BuildRaw() 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) + t_insert(rawLines, "Prefix: " .. (affix.fractured and "{fractured}" or "") .. (affix.range and ("{range:" .. round(affix.range,3) .. "}") or "") .. affix.modId) end for i, affix in ipairs(self.suffixes or { }) do - t_insert(rawLines, "Suffix: " .. (affix.range and ("{range:" .. round(affix.range,3) .. "}") or "") .. affix.modId) + t_insert(rawLines, "Suffix: " .. (affix.fractured and "{fractured}" or "") .. (affix.range and ("{range:" .. round(affix.range,3) .. "}") or "") .. affix.modId) end end if self.catalyst and self.catalyst > 0 then @@ -1522,10 +1616,13 @@ function ItemClass:BuildRaw() if self.itemLevel then t_insert(rawLines, "Item Level: " .. self.itemLevel) end + if self.memoryStrands then + t_insert(rawLines, "Memory Strands: " .. self.memoryStrands) + end local function writeModLine(modLine) local line = modLine.line if modLine.range and line:match("%(%-?[%d%.]+%-%-?[%d%.]+%)") then - line = "{range:" .. round(modLine.range, 3) .. "}" .. line + line = "{range:" .. round(modLine.range, 6) .. "}" .. line end if modLine.corruptedRange then line = "{corruptedRange:" .. round(modLine.corruptedRange, 2) .. "}" .. line @@ -1715,9 +1812,8 @@ function ItemClass:Craft() self.nameSuffix = self.nameSuffix .. " " .. mod.affix end self.requirements.level = m_max(self.requirements.level or 0, m_floor(mod.level * 0.8)) - local rangeScalar = getCatalystScalar(self.catalyst, mod, self.catalystQuality) for i, line in ipairs(mod) do - line = itemLib.applyRange(line, affix.range or 0.5, rangeScalar) + line = itemLib.applyRange(line, affix.range or 0.5) local order = mod.statOrder[i] if statOrder[order] then -- Combine stats @@ -1728,18 +1824,13 @@ function ItemClass:Craft() return tonumber(num) + tonumber(other) end) else - local modLine = { line = line, order = order, type = mod.type } + local modLine = { line = line, order = order, type = mod.type, modTags = mod.modTags or { }, fractured = affix.fractured } if mod.type == "Prefix" then modLine.prefix = true elseif mod.type == "Suffix" then modLine.suffix = true end - for l = 1, #self.explicitModLines + 1 do - if not self.explicitModLines[l] or self.explicitModLines[l].order > order then - t_insert(self.explicitModLines, l, modLine) - break - end - end + t_insert(self.explicitModLines, modLine) statOrder[order] = modLine end end @@ -1751,6 +1842,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/ItemsTab.lua b/src/Classes/ItemsTab.lua index 7171ea2db2..35a69fa86d 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -734,7 +734,7 @@ holding Shift will put it in the second.]]) return range end drop = new("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 @@ -1844,10 +1844,11 @@ function ItemsTabClass:SetDisplayItem(item) end end end - self.controls.displayItemInfluence:SetSel(influence1, true) -- Don't call the selection function for the first influence dropdown as the second dropdown isn't properly set yet. - self.controls.displayItemInfluence2:SetSel(influence2) -- The selection function for the second dropdown properly handles everything for both dropdowns + -- Initialising these controls must not re-craft the parsed item. + self.controls.displayItemInfluence:SetSel(influence1, true) + self.controls.displayItemInfluence2:SetSel(influence2, true) 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 @@ -2053,7 +2054,9 @@ 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 range = m_min(1, m_max(0, item[outputTable][outputIndex].range 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 @@ -4272,7 +4275,6 @@ function ItemsTabClass:AddItemTooltip(tooltip, item, slot, dbMode, maxWidth) end end end - if item.catalyst and item.catalyst > 0 and item.catalyst <= #catalystQualityFormat and item.catalystQuality and item.catalystQuality > 0 then tooltip:AddLine(fontSizeBig, s_format(catalystQualityFormat[item.catalyst], item.catalystQuality), "FONTIN SC") tooltip:AddSeparator(10) @@ -4309,6 +4311,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/Data/ModCache.lua b/src/Data/ModCache.lua index dfdc865eb2..4b18c1d056 100755 --- a/src/Data/ModCache.lua +++ b/src/Data/ModCache.lua @@ -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}