diff --git a/spec/System/TestTinctureImport_spec.lua b/spec/System/TestTinctureImport_spec.lua new file mode 100644 index 0000000000..f0132df2eb --- /dev/null +++ b/spec/System/TestTinctureImport_spec.lua @@ -0,0 +1,72 @@ +describe("TestTinctureImport", function() + local function newImportedTincture(baseName, quality, implicitLines, explicitLines) + local item = new("Item") + item.baseName = baseName + item.base = data.itemBases[baseName] + item.type = "Tincture" + item.name = baseName + item.rarity = "MAGIC" + item.quality = quality + item.implicitModLines = { } + item.explicitModLines = { } + item.enchantModLines = { } + item.scourgeModLines = { } + item.classRequirementModLines = { } + item.crucibleModLines = { } + for _, line in ipairs(implicitLines or { }) do + table.insert(item.implicitModLines, { line = line }) + end + for _, line in ipairs(explicitLines or { }) do + table.insert(item.explicitModLines, { line = line }) + end + return item + end + + it("normalises Rosethorn import lines back to base values", function() + local item = newImportedTincture("Rosethorn Tincture", 20, { + "216% increased Critical Strike Chance with Melee Weapons", + }, { + "20% increased effect", + "36% increased Melee Weapon Attack Speed", + }) + + item:NormaliseImportedTinctureModLines() + item:BuildAndParseRaw() + + assert.are.equal("150% increased Critical Strike Chance with Melee Weapons", item.implicitModLines[1].line) + assert.are.equal("20% increased effect", item.explicitModLines[1].line) + assert.are.equal("25% increased Melee Weapon Attack Speed", item.explicitModLines[2].line) + end) + + it("preserves non-scaled Mana Burn lines while normalising effect-scaled tincture lines", function() + local item = newImportedTincture("Prismatic Tincture", 20, { + "162% increased Elemental Damage with Melee Weapons", + }, { + "35% increased effect", + "37% increased Mana Burn rate", + }) + + item:NormaliseImportedTinctureModLines() + item:BuildAndParseRaw() + + assert.are.equal("100% increased Elemental Damage with Melee Weapons", item.implicitModLines[1].line) + assert.are.equal("35% increased effect", item.explicitModLines[1].line) + assert.are.equal("37% increased Mana Burn rate", item.explicitModLines[2].line) + end) + + it("can recover the base local effect roll when the imported effect line is already quality-scaled", function() + local item = newImportedTincture("Prismatic Tincture", 20, { + "162% increased Elemental Damage with Melee Weapons", + }, { + "42% increased effect", + "37% increased Mana Burn rate", + }) + + item:NormaliseImportedTinctureModLines() + item:BuildAndParseRaw() + + assert.are.equal("100% increased Elemental Damage with Melee Weapons", item.implicitModLines[1].line) + assert.are.equal("35% increased effect", item.explicitModLines[1].line) + assert.are.equal("37% increased Mana Burn rate", item.explicitModLines[2].line) + end) +end) diff --git a/src/Classes/ImportTab.lua b/src/Classes/ImportTab.lua index 1427ff4a97..0e22ccd25b 100644 --- a/src/Classes/ImportTab.lua +++ b/src/Classes/ImportTab.lua @@ -1083,6 +1083,10 @@ function ImportTabClass:ImportItem(itemData, slotName) item.foilType = foilVariants[itemData.foilVariation] or "Rainbow" end + if item.base and item.base.tincture then + item:NormaliseImportedTinctureModLines() + end + -- Add and equip the new item item:BuildAndParseRaw() --ConPrintf("%s", item.raw) @@ -1231,4 +1235,4 @@ function ImportTabClass:SetPredefinedBuildName() local charData = charSelect.list[charSelect.selIndex].char local charName = charData.name main.predefinedBuildName = accountName.." - "..charName -end \ No newline at end of file +end diff --git a/src/Classes/Item.lua b/src/Classes/Item.lua index 4597dde52b..062338d213 100644 --- a/src/Classes/Item.lua +++ b/src/Classes/Item.lua @@ -6,6 +6,7 @@ local ipairs = ipairs local t_insert = table.insert local t_remove = table.remove +local m_abs = math.abs local m_min = math.min local m_max = math.max local m_floor = math.floor @@ -962,6 +963,175 @@ function ItemClass:NormaliseQuality() end end +local tinctureLocalModNames = { + CooldownRecovery = true, + LocalEffect = true, + TinctureCooldownRecovery = true, + TinctureEffect = true, + TinctureManaBurnRate = true, +} + +local function getTinctureModLineParse(line) + if not line then + return + end + return modLib.parseMod(line:gsub("\n", " ")) +end + +local function getTinctureModLineSignature(modList) + local signature = { } + for _, mod in ipairs(modList or { }) do + t_insert(signature, modLib.formatMod(mod)) + end + table.sort(signature) + return table.concat(signature, "\n") +end + +local function tinctureModLineShouldScale(modList) + for _, mod in ipairs(modList or { }) do + local scaledMod = (type(mod.value) == "table" and mod.value.mod) or mod + if tinctureLocalModNames[scaledMod.name] then + return false + end + end + return true +end + +local function tinctureModLineHasLocalEffect(modList) + for _, mod in ipairs(modList or { }) do + local scaledMod = (type(mod.value) == "table" and mod.value.mod) or mod + if scaledMod.name == "LocalEffect" or scaledMod.name == "TinctureEffect" then + return true + end + end + return false +end + +local function getTinctureRangeSteps(templateLine) + local maxSteps = 0 + for min, max in templateLine:gmatch("%((%-?%d+%.?%d*)%-(%-?%d+%.?%d*)%)") do + local minStr, maxStr = tostring(min), tostring(max) + local minPrecision = minStr:match("%.(%d+)") and #minStr:match("%.(%d+)") or 0 + local maxPrecision = maxStr:match("%.(%d+)") and #maxStr:match("%.(%d+)") or 0 + local power = 10 ^ m_max(minPrecision, maxPrecision) + local steps = m_floor(m_abs((tonumber(max) - tonumber(min)) * power) + 0.5) + maxSteps = m_max(maxSteps, steps) + end + return maxSteps +end + +local function getTinctureScaledModList(modList, scale) + if scale == 1 then + return modList + end + local scaledList = new("ModList") + scaledList:ScaleAddList(modList, scale) + return { unpack(scaledList) } +end + +local function matchTinctureModLine(targetLine, templateLines, scale, forceScale) + local targetModList, targetExtra = getTinctureModLineParse(targetLine) + if not targetModList or targetExtra or not templateLines then + return + end + local targetShouldScale = forceScale or tinctureModLineShouldScale(targetModList) + local targetSignature = getTinctureModLineSignature(targetModList) + for _, templateLine in ipairs(templateLines) do + local steps = getTinctureRangeSteps(templateLine) + local seen = { } + for step = 0, m_max(steps, 0) do + local candidateLine = steps > 0 and itemLib.applyRange(templateLine, step / steps) or templateLine + if not seen[candidateLine] then + seen[candidateLine] = true + local candidateModList, candidateExtra = getTinctureModLineParse(candidateLine) + if candidateModList and not candidateExtra then + if targetShouldScale and scale ~= 1 then + candidateModList = getTinctureScaledModList(candidateModList, scale) + end + if getTinctureModLineSignature(candidateModList) == targetSignature then + return candidateLine + end + end + end + end + end +end + +function ItemClass:NormaliseImportedTinctureModLines() + if not self.base or not self.base.tincture then + return + end + + local explicitTemplateLines = { } + if self.rarity == "UNIQUE" or self.rarity == "RELIC" then + local uniqueItem = main and main.uniqueDB and main.uniqueDB.list and main.uniqueDB.list[self.name] + if uniqueItem then + for _, modLine in ipairs(uniqueItem.explicitModLines) do + t_insert(explicitTemplateLines, modLine.line) + end + end + else + local affixes = self.affixes + or (self.base.subType and data.itemMods[self.base.type .. self.base.subType]) + or data.itemMods[self.base.type] + or data.itemMods.Item + for _, affix in pairs(affixes or { }) do + for _, line in ipairs(affix) do + t_insert(explicitTemplateLines, line) + end + end + end + + local localEffectInc = 0 + for _, modLine in ipairs(self.explicitModLines) do + local modList = getTinctureModLineParse(modLine.line) + local matchedLine + if tinctureModLineHasLocalEffect(modList) then + matchedLine = matchTinctureModLine(modLine.line, explicitTemplateLines, 1) + if not matchedLine and (self.quality or 0) > 0 then + matchedLine = matchTinctureModLine(modLine.line, explicitTemplateLines, 1 + (self.quality or 0) / 100, true) + end + end + if matchedLine then + modLine.line = matchedLine + end + modList = getTinctureModLineParse(modLine.line) + for _, mod in ipairs(modList or { }) do + local scaledMod = (type(mod.value) == "table" and mod.value.mod) or mod + if scaledMod.type == "INC" and (scaledMod.name == "LocalEffect" or scaledMod.name == "TinctureEffect") then + localEffectInc = localEffectInc + scaledMod.value + end + end + end + + local effectMod = m_floor((1 + localEffectInc / 100) * (1 + (self.quality or 0) / 100) * 100 + 0.0001) / 100 + if effectMod == 1 then + return + end + + local implicitTemplateLines = { } + if self.base.implicit then + for line in self.base.implicit:gmatch("[^\n]+") do + t_insert(implicitTemplateLines, line) + end + end + + for index, modLine in ipairs(self.implicitModLines) do + local templateLines = implicitTemplateLines[index] and { implicitTemplateLines[index] } or implicitTemplateLines + local matchedLine = matchTinctureModLine(modLine.line, templateLines, effectMod) + if matchedLine then + modLine.line = matchedLine + end + end + + for _, modLine in ipairs(self.explicitModLines) do + local matchedLine = matchTinctureModLine(modLine.line, explicitTemplateLines, effectMod) + if matchedLine then + modLine.line = matchedLine + end + end +end + function ItemClass:GetModSpawnWeight(mod, includeTags, excludeTags) local weight = 0 if self.base then