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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions spec/System/TestItemMods_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,92 @@ describe("TetsItemMods", function()
assert.are.equals(10, build.calcsTab.calcsOutput.PhysicalLifeRecoup)
assert.are.equals(10, build.calcsTab.calcsOutput.PhysicalEnergyShieldRecoup)
end)

it("crafts modifiers from supported bases on rare-like uniques", function()
local item = new("Item", [[
Item Class: Helmets
Rarity: Unique
Subsume the Source
Faithful Helmet
--------
Item Level: 86
--------
{ Prefix Modifier "Hale" }
+23 to maximum Life
{ Unique Modifier }
120% increased Explicit Modifier magnitudes
{ Suffix Modifier "of Adaption" }
+7 to all Attributes
{ Unique Modifier }
Cannot have non-Abyssal sockets
]])

assert.are.equals(4, item.affixLimit)
assert.are.equals(4, item.prefixes.limit)
assert.are.equals(0, item.suffixes.limit)
assert.are.equals("AbyssJewelAddedLife1", item.prefixes[1].modId)
assert.are.equals("AbyssAllAttributesJewel1", item.prefixes[2].modId)
build.itemsTab.displayItem = item
build.itemsTab:UpdateAffixControls()
local lifeModAvailable = false
for _, entry in ipairs(build.itemsTab.controls.displayItemAffix2.list) do
for _, modId in ipairs(type(entry) == "table" and entry.modList or { }) do
lifeModAvailable = lifeModAvailable or modId == "AbyssJewelAddedLife1"
end
end
assert.is_true(lifeModAvailable)

item:Craft()
local raw = item:BuildRaw()
assert.is_truthy(raw:find("increased Explicit Modifier magnitudes", 1, true))
assert.is_truthy(raw:find("Cannot have non-Abyssal sockets", 1, true))
end)

it("uses the item base for rare-like modifier eligibility by default", function()
local item = new("Item", [[
Item Class: Bows
Rarity: Unique
The Crimson Storm
Steelwood Bow
--------
Item Level: 85
--------
{ Suffix Modifier "of the Order" }
+24(24-28)% to Physical Damage over Time Multiplier
]])

assert.are.equals(1, item.affixLimit)
assert.are.equals(0, item.prefixes.limit)
assert.are.equals(1, item.suffixes.limit)
assert.are.equals("JunMasterVeiledPhysicalDamageOverTimeMultiplier", item.suffixes[1].modId)
end)

it("keeps modifier metadata on duplicate variant tooltip lines", function()
local item = new("Item", [[
Rarity: Unique
Duplicate Variant Test
Plate Vest
Variant: First
Variant: Second
Selected Variant: 1
Has Alt Variant: true
Selected Alt Variant: 1
Allow Duplicate Variants: true
Implicits: 0
{variant:1}+10 to maximum Life
]])
local tooltip = new("Tooltip")
build.itemsTab:AddItemTooltip(tooltip, item)

local count = 0
for _, line in ipairs(tooltip.lines) do
if line.text and line.text:find("maximum Life", 1, true) then
assert.are.equals(item.explicitModLines[1], line.modLine)
count = count + 1
end
end
assert.are.equals(2, count)
end)

it("shows a fallback tooltip when an item's base is no longer supported", function()
local item = new("Item", [[
Expand Down
81 changes: 68 additions & 13 deletions src/Classes/Item.lua
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,7 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
local implicitLines = 0
self.variantList = nil
self.versionList = nil
self.allowDuplicateVariants = false
self.variantGroups = { }
self.variantGroupSelections = self.variantGroupSelections or { }
self.usesVariantGroups = false
Expand All @@ -494,6 +495,8 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
self.baseLines = { }
self.foulborn = false
self.mutatedLines = nil
---@type RareLikeUniqueDescription?
self.rareLikeUnique = nil
local importedLevelReq
local flaskBuffLines
local tinctureBuffLines
Expand Down Expand Up @@ -563,15 +566,16 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
local backupAffixList = { }
for modId, modData in pairs(self.affixes) do
if modData.affix == modName then
local ignoreModType = self.rareLikeUnique and self.rareLikeUnique.ignoreModType
if self:CanHaveMod(modData) then
if modData.type == "Prefix" then
if modData.type == "Prefix" or ignoreModType then
t_insert(self.pendingAffixList, { modId = modId, table = self.prefixes })
elseif modData.type == "Suffix" then
t_insert(self.pendingAffixList, { modId = modId, table = self.suffixes })
end
else
-- Conqueror mods can't natively spawn on items, so we'll use those if we don't find a match otherwise
if modData.type == "Prefix" then
if modData.type == "Prefix" or ignoreModType then
t_insert(backupAffixList, { modId = modId, table = self.prefixes })
elseif modData.type == "Suffix" then
t_insert(backupAffixList, { modId = modId, table = self.suffixes })
Expand Down Expand Up @@ -752,6 +756,8 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
self.variantAlt4 = specToNumber(specVal)
elseif specName == "Selected Alt Variant Five" then
self.variantAlt5 = specToNumber(specVal)
elseif specName == "Allow Duplicate Variants" then
self.allowDuplicateVariants = specVal == "true"
elseif specName == "Has Variants" or specName == "Selected Variants" then
-- Need to skip this line for backwards compatibility
-- with builds that used an old Watcher's Eye implementation
Expand Down Expand Up @@ -1000,6 +1006,12 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
self.affixes = (self.base.subType and data.itemMods[self.base.type..self.base.subType])
or data.itemMods[self.base.type]
or data.itemMods.Item
if self.title then
self.rareLikeUnique = data.rareLikeUniques[self.title:lower()]
if self.rareLikeUnique then
self.affixes = self.rareLikeUnique.affixes
end
end
if self.base.flask then
if self.base.utility_flask then
self.enchantments = data.enchantments["UtilityFlask"]
Expand Down Expand Up @@ -1372,6 +1384,10 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
end
for _, mods in ipairs(modLists) do
for _, mod in ipairs(mods or {}) do
-- avoid scaling variant lines which are not active
if mod.variantList and (self:GetModLineVariantCount(mod) == 0) then
goto modMagnitudeContinue
end
-- Create a fast lookup table for all provided tags
local tagLookup = {}
for _, curTag in ipairs(mod.modTags) do
Expand Down Expand Up @@ -1400,12 +1416,13 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
end
end
if mod.valueScalar and mod.valueScalar ~= 1 then
local rangedLine = itemLib.applyRange(mod.line, mod.range, mod.valueScalar, 1)
local rangedLine = itemLib.applyRange(mod.line, mod.range or 1, mod.valueScalar, 1)
local modList, extra = modLib.parseMod(rangedLine)
mod.displayValueScalar = 1
mod.modList = modList
mod.extra = extra
end
::modMagnitudeContinue::
end
end
end
Expand Down Expand Up @@ -1433,6 +1450,10 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
self.suffixes.limit = m_max(m_min((self.suffixes.limit or 0) + self.affixLimit / 2, self.affixLimit), 0)
self.affixLimit = self.prefixes.limit + self.suffixes.limit
end
elseif self.rareLikeUnique then
self.prefixes.limit = self.rareLikeUnique.prefixLimit
self.suffixes.limit = self.rareLikeUnique.suffixLimit
self.affixLimit = self.prefixes.limit + self.suffixes.limit
else
self.crafted = false
end
Expand Down Expand Up @@ -1628,9 +1649,10 @@ function ItemClass:NormaliseQuality()
end
end

function ItemClass:GetModSpawnWeight(mod, includeTags, excludeTags)
function ItemClass:GetModSpawnWeight(mod, includeTags, excludeTags, baseTags)
local weight = 0
if self.base then
baseTags = baseTags or self.base.tags
local function HasInfluenceTag(key)
if self.base.influenceTags then
for _, curInfluenceInfo in ipairs(influenceInfo) do
Expand Down Expand Up @@ -1673,13 +1695,13 @@ function ItemClass:GetModSpawnWeight(mod, includeTags, excludeTags)
end

for i, key in ipairs(mod.weightKey) do
if (self.base.tags[key] or (includeTags and includeTags[key]) or HasInfluenceTag(key)) and not (excludeTags and excludeTags[key]) then
if (baseTags[key] or (includeTags and includeTags[key]) or HasInfluenceTag(key)) and not (excludeTags and excludeTags[key]) then
weight = (HasInfluenceTag(key) and HasMavenInfluence(mod.affix)) and 1000 or mod.weightVal[i]
break
end
end
for i, key in ipairs(mod.weightMultiplierKey or {}) do
if (self.base.tags[key] or (includeTags and includeTags[key]) or HasInfluenceTag(key)) and not (excludeTags and excludeTags[key]) then
if (baseTags[key] or (includeTags and includeTags[key]) or HasInfluenceTag(key)) and not (excludeTags and excludeTags[key]) then
weight = weight * mod.weightMultiplierVal[i] / 100
break
end
Expand Down Expand Up @@ -1910,6 +1932,9 @@ function ItemClass:BuildRaw()
t_insert(rawLines, "Has Alt Variant Five: true")
t_insert(rawLines, "Selected Alt Variant Five: " .. self.variantAlt5)
end
if self.allowDuplicateVariants then
t_insert(rawLines, "Allow Duplicate Variants: true")
end
end
if not self.variantList then
for _, baseLine in pairs(self.baseLines or { }) do
Expand Down Expand Up @@ -1990,7 +2015,7 @@ function ItemClass:Craft()
-- Save off any crafted or custom mods so they can be re-added at the end
local savedMods = {}
for _, mod in ipairs(self.explicitModLines) do
if mod.crafted or mod.custom then
if mod.crafted or mod.custom or (self.rareLikeUnique and not (mod.prefix or mod.suffix)) then
t_insert(savedMods, mod)
end
end
Expand All @@ -2000,7 +2025,7 @@ function ItemClass:Craft()
self.nameSuffix = ""
self.requirements.level = self.base.req.level
local statOrder = { }
for _, list in ipairs({self.prefixes,self.suffixes}) do
for _, list in ipairs({ self.prefixes, self.suffixes }) do
for i = 1, (list.limit or (self.affixLimit / 2)) do
local affix = list[i]
if not affix then
Expand Down Expand Up @@ -2079,6 +2104,23 @@ function ItemClass:CheckModLineVariant(modLine)
or (self.hasAltVariant5 and modLine.variantList[self.variantAlt5])
end

function ItemClass:GetModLineVariantCount(modLine)
if not self.allowDuplicateVariants or not modLine.variantList then
return self:CheckModLineVariant(modLine) and 1 or 0
end

-- Mageblood can intentionally select the same variant more than once.
local variantList = modLine.variantList
local count = variantList[self.variant] and 1 or 0
for i = 1, 5 do
local suffix = i == 1 and "" or i
local variant = self["variantAlt" .. suffix]
if self["hasAltVariant" .. suffix] and variant and variantList[variant] then
count = count + 1
end
end
return count
end
-- Return the name of the slot this item is equipped in
function ItemClass:GetPrimarySlot()
if self.base.weapon then
Expand Down Expand Up @@ -2468,7 +2510,8 @@ function ItemClass:BuildModList()
if modLine.disabled then
return
end
if self:CheckModLineVariant(modLine) then
local variantCount = self:GetModLineVariantCount(modLine)
if variantCount > 0 then
-- special section for variant over-ride of pre-modifier item parameters
if modLine.line:find("Requires Class") then
self.classRestriction = modLine.line:gsub("{variant:([%d,]+)}", ""):match("Requires Class (.+)")
Expand All @@ -2485,8 +2528,9 @@ function ItemClass:BuildModList()
end
if not modLine.extra then
for _, mod in ipairs(modLine.modList) do
mod = modLib.setSource(mod, self.modSource)
baseList:AddMod(mod)
for _ = 1, variantCount do
baseList:AddMod(modLib.setSource(mod, self.modSource))
end
end
if modLine.modTags and #modLine.modTags > 0 then
self.hasModTags = true
Expand Down Expand Up @@ -2546,6 +2590,7 @@ function ItemClass:BuildModList()
-- Remove all sockets
wipeTable(self.sockets)
self.selectableSocketCount = 0
self.abyssalSocketCount = 0
elseif socketCount > 0 then
-- Force the socket count to be equal to the stated number
self.selectableSocketCount = socketCount
Expand Down Expand Up @@ -2604,8 +2649,9 @@ function ItemClass:BuildModList()
end
end

function ItemClass:CanHaveMod(mod)
local keyMap, includeTags = { }, { }
function ItemClass:CanHaveMod(mod, includeTags)
local keyMap = { }
includeTags = includeTags or { }
for index, key in ipairs(mod.weightKey) do
keyMap[key] = index
end
Expand All @@ -2616,6 +2662,15 @@ function ItemClass:CanHaveMod(mod)
if data.minionTagCrucibleUniques[self.title] then
includeTags["minion_unique_weapon"] = true
end
if self.rareLikeUnique and self.rareLikeUnique.validBases then
-- Some uniques use modifiers from another item type.
for _, base in ipairs(self.rareLikeUnique.validBases) do
if self:GetModSpawnWeight(mod, includeTags, nil, base.base.tags) > 0 then
return true
end
end
return false
end
if self.canHaveOnlySupportSkillsCrucibleTree then
return keyMap["crucible_unique_staff"] and mod.weightVal[keyMap["crucible_unique_staff"]] ~= 0
elseif self.canHaveShieldCrucibleTree then
Expand Down
Loading
Loading