Skip to content
Open
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
23 changes: 23 additions & 0 deletions spec/System/TestItemParse_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1306,6 +1306,29 @@ describe("TestAdvancedItemParse #item", function()
assert.are.equals(195, chaosDamageInc())
end)

it("calculates catalyst and magnitude scaling for a proposed explicit mod", function()
local item = new("Item"):Item([[
Rarity: RARE
Test Subject
Sapphire Ring
Catalyst: Intrinsic
CatalystQuality: 20
Implicits: 0
{range:0.5}50% increased effect of prefixes
]])
item.modMagnitudeMods = {
{ tags = { "prefix" }, multiplier = 2 },
{ tags = { "prefix" }, quality = 50 },
}
local attributePrefix = { modTags = { "attribute" }, prefix = true }
local attributeSuffix = { modTags = { "attribute" }, suffix = true }

assert.are.equals(2.9, item:GetModLineValueScalar(attributePrefix, "explicit"))
assert.are.equals(1.2, item:GetModLineValueScalar(attributeSuffix, "explicit"))
attributePrefix.unscalable = true
assert.are.equals(1, item:GetModLineValueScalar(attributePrefix, "explicit"))
end)

-- actually a ring so we don't have to allocate a socket
local realJewel = [[
Rarity: Rare
Expand Down
154 changes: 153 additions & 1 deletion spec/System/TestTradeQueryGenerator_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,108 @@ describe("TradeQueryGenerator", function()
end)
end)

describe("EstimateBenchCraftWeight", function()
local queryGen

local function makeCraft(spec)
local lines = spec.lines or { spec.line }
spec.line, spec.lines = nil, nil
for _, line in ipairs(lines) do table.insert(spec, line) end
spec.types = spec.types or { Ring = true }
return spec
end

local function addWeightedTradeMod(spec)
queryGen.modData.Explicit[spec.statOrder .. "_" .. spec.group] =
{ tradeMod = { id = spec.id, text = spec.text } }
table.insert(queryGen.modWeights, { tradeModId = spec.id, weight = spec.weight })
end

before_each(function()
queryGen = new("TradeQueryGenerator"):TradeQueryGenerator({ itemsTab = { } })
queryGen.modData = { Explicit = { } }
queryGen.modWeights = { }
end)

it("adds the weighted values of every craft line", function()
addWeightedTradeMod({ statOrder = 1203, group = "TestAttributes", id = "explicit.stat_4080418644",
text = "+# to Strength", weight = 2 })
addWeightedTradeMod({ statOrder = 1204, group = "TestAttributes", id = "explicit.stat_3261801346",
text = "+# to Dexterity", weight = 3 })
local craft = makeCraft({ lines = { "+(10-10) to Strength", "+(20-20) to Dexterity" },
statOrder = { 1203, 1204 }, group = "TestAttributes" })
local evaluationPlan = queryGen:CreateBenchCraftEvaluationPlan({ { stat = "Life", weightMult = 1 } })

assert.are.equal(80, queryGen:EstimateBenchCraftWeight(craft, evaluationPlan))
assert.are.equal(96, queryGen:EstimateBenchCraftWeight(craft, evaluationPlan, 1.2))
assert.are.equal(120, queryGen:EstimateBenchCraftWeight(craft, evaluationPlan, 1.5))
assert.are.equal(80, evaluationPlan.craftWeights[craft])
end)

it("keeps the first craft when the highest levels in a group are tied", function()
local low = makeCraft({ group = "Test", level = 1 })
local firstHigh = makeCraft({ group = "Test", level = 2 })
local secondHigh = makeCraft({ group = "Test", level = 2 })

local crafts = queryGen:GetHighestLevelBenchCrafts({ low, firstHigh, secondHigh })

assert.are.same({ firstHigh }, crafts)
end)

it("keeps generated mod and stat weights immutable", function()
queryGen.modWeights = { { tradeModId = "explicit.test", weight = 2 } }
local statWeights = { { stat = "Life", weightMult = 1 } }

local evaluationPlan = queryGen:CreateBenchCraftEvaluationPlan(statWeights)
queryGen.modWeights[1].weight = 20
statWeights[1].weightMult = 10

assert.are.equal(2, evaluationPlan.weightsByTradeMod["explicit.test"].weight)
assert.are.equal(1, evaluationPlan.statWeights[1].weightMult)
end)

it("finds the best positive compatible bench craft query weight", function()
local prefixLow = makeCraft({ type = "Prefix", level = 1, statOrder = { 1203 },
group = "TestStrength", line = "+(10-10) to Strength" })
local prefixHigh = makeCraft({ type = "Prefix", level = 2, statOrder = { 1203 },
group = "TestStrength", line = "+(20-20) to Strength" })
local suffix = makeCraft({ type = "Suffix", statOrder = { 1204 },
group = "TestDexterity", line = "+(10-10) to Dexterity" })
local incompatible = makeCraft({ type = "Prefix", types = { Amulet = true }, statOrder = { 1203 },
group = "TestStrength", line = "+(100-100) to Strength" })
local negative = makeCraft({ type = "Prefix", types = { Belt = true }, statOrder = { 1205 },
group = "TestIntelligence", line = "+(10-10) to Intelligence" })
queryGen.itemsTab.build = {
data = { masterMods = { prefixLow, prefixHigh, suffix, incompatible, negative } },
}
addWeightedTradeMod({ statOrder = 1203, group = "TestStrength", id = "explicit.stat_4080418644",
text = "+# to Strength", weight = 2 })
addWeightedTradeMod({ statOrder = 1204, group = "TestDexterity", id = "explicit.stat_3261801346",
text = "+# to Dexterity", weight = 3 })
addWeightedTradeMod({ statOrder = 1205, group = "TestIntelligence", id = "explicit.stat_328541901",
text = "+# to Intelligence", weight = -4 })
local evaluationPlan = queryGen:CreateBenchCraftEvaluationPlan({ })

local weight = queryGen:GetBenchCraftQueryWeight({ type = "Ring" }, evaluationPlan)

assert.are.equal(40, weight)
assert.is_nil(queryGen:GetBenchCraftQueryWeight({ type = "Belt" }, evaluationPlan))
end)

it("weights only items with exactly one empty affix", function()
queryGen.GetBenchCraftQueryWeight = function() return 60 end

local filter, priority = queryGen:GetBenchCraftQueryFilter({ type = "Ring" }, { })

assert.are.equal("pseudo.pseudo_number_of_empty_affix_mods", filter.id)
assert.are.same({ min = 1, max = 1, weight = 60 }, filter.value)
assert.are.equal(60, priority)
assert.is_nil(filter.priority)
end)
end)

describe("Filter prioritization", function()
it("counts socket and link constraints against MAX_FILTERS", function()
it("counts socket and link constraints against the complexity budget", function()
local queryGen = new("TradeQueryGenerator"):TradeQueryGenerator({ itemsTab = { items = {} } })
queryGen.modWeights = { }
for index = 1, 40 do
Expand Down Expand Up @@ -203,5 +303,57 @@ describe("TradeQueryGenerator", function()
assert.is_not_nil(query.filters.socket_filters.filters.sockets)
assert.is_not_nil(query.filters.socket_filters.filters.links)
end)

it("ranks the single empty affix weight with regular filters before applying the complexity budget", function()
local queryGen = new("TradeQueryGenerator"):TradeQueryGenerator({ itemsTab = { items = { } } })
queryGen.modWeights = { }
for index = 1, 40 do
table.insert(queryGen.modWeights, {
tradeModId = "explicit.stat_" .. index,
weight = 1,
meanStatDiff = 41 - index,
})
end
queryGen.calcContext = {
testItem = new("Item"):Item("Rarity: RARE\nNew Item\nGold Ring\nImplicits: 0"),
baseOutput = { },
baseStatValue = 0,
itemCategoryQueryStr = "accessory.ring",
special = { },
options = {
statWeights = { },
influence1 = 1,
influence2 = 1,
includeMirrored = false,
sockets = 6,
links = 6,
},
}
queryGen.tradeTypeIndex = 1
queryGen.requesterContext = { slotTbl = { considerBenchCraft = true } }
local receivedPlan
queryGen.GetBenchCraftQueryFilter = function(_, _, evaluationPlan)
receivedPlan = evaluationPlan
return {
id = "pseudo.pseudo_number_of_empty_affix_mods",
value = { min = 1, max = 1, weight = 35.5 },
}, 35.5
end
local query
queryGen.requesterCallback = function(_, queryJson)
query = require("dkjson").decode(queryJson).query
end

queryGen:FinishQuery()
local filtersById = { }
for _, filter in ipairs(query.stats[1].filters) do
filtersById[filter.id] = filter
end
assert.are.equal(31, #query.stats[1].filters)
assert.are.equal(35.5, filtersById["pseudo.pseudo_number_of_empty_affix_mods"].value.weight)
assert.is_not_nil(filtersById["explicit.stat_30"])
assert.is_nil(filtersById["explicit.stat_31"])
assert.are.equal(receivedPlan, queryGen.requesterContext.benchCraftEvaluationPlan)
end)
end)
end)
160 changes: 160 additions & 0 deletions spec/System/TestTradeQueryRequests_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,166 @@ Strict-Transport-Security: max-age=63115200; includeSubDomains; preload]]
assert.are.equal("42", itemsById.legacy.weight)
assert.are.equal("0", itemsById.empty.weight)
end)

it("reconstructs explicit, crafted, and fractured affixes for bench craft replacement", function()
local function makeTradeApiMod(description, hash, tier, domain)
return {
description = description, domain = domain or "explicit", hash = "stat." .. hash,
mods = { { name = "Test Affix", tier = tier, level = 30 } },
}
end
local response = dkjson.encode({
result = { {
id = "affix-metadata",
listing = { price = { amount = 1, currency = "chaos", type = "~price" },
whisper = "hi", account = { name = "seller" } },
item = {
rarity = "Rare", name = "Test Band", typeLine = "Sapphire Ring",
explicitMods = {
makeTradeApiMod("+50 to maximum Life", "explicit.life", "P2"),
{ description = "+25 to maximum Mana", domain = "fractured", hash = "stat.fractured.mana",
flags = { fractured = true }, mods = { { name = "Test Affix", tier = "P2", level = 30 } } },
makeTradeApiMod("20% increased Armour", "explicit.armour", "P2"),
makeTradeApiMod("+30% to Fire Resistance", "explicit.fire", "S3"),
makeTradeApiMod("+30% to Cold Resistance", "explicit.cold", "S3"),
makeTradeApiMod("+20 to Dexterity", "crafted.dexterity", "S3", "crafted"),
makeTradeApiMod("10% increased Rarity of Items found", "crafted.rarity", "S3", "crafted"),
},
extended = { hashes = {
explicit = {
{ "explicit.life", { 0 } }, { "explicit.armour", { 0 } },
{ "explicit.fire", { 1 } }, { "explicit.cold", { 2 } },
},
crafted = { { "crafted.dexterity", { 0 } }, { "crafted.rarity", { 0 } } },
fractured = { { "fractured.mana", { 0 } } },
} },
},
} },
})
local fetchedItems
requests.requestQueue.fetch = { }
requests:FetchResultBlock("test", function(items) fetchedItems = items end)

local request = table.remove(requests.requestQueue.fetch, 1)
request.callback(response)

local item = new("Item"):Item(fetchedItems[1].item_string)
local modLines = item.explicitModLines
assert.are.same({ true, true, true }, { modLines[1].prefix, modLines[2].prefix, modLines[3].prefix })
assert.are_not.equal(modLines[1].modGroup, modLines[2].modGroup)
assert.are.equal("trade:fractured:0", modLines[2].modGroup)
assert.is_true(modLines[2].fractured)
assert.are.equal(modLines[1].modGroup, modLines[3].modGroup)
assert.are.same({ true, true, true, true },
{ modLines[4].suffix, modLines[5].suffix, modLines[6].suffix, modLines[7].suffix })
assert.are_not.equal(modLines[4].modGroup, modLines[5].modGroup)
assert.are.same({ true, true }, { modLines[6].crafted, modLines[7].crafted })
assert.are.equal(modLines[6].modGroup, modLines[7].modGroup)

local tradeQuery = new("TradeQuery"):TradeQuery({ itemsTab = { } })
local strippedItem = new("Item"):Item(item:BuildRaw())
for index = #strippedItem.explicitModLines, 1, -1 do
if strippedItem.explicitModLines[index].crafted then
table.remove(strippedItem.explicitModLines, index)
end
end
strippedItem = new("Item"):Item(strippedItem:BuildRaw())
assert.are.same({ Prefix = 1, Suffix = 1 }, tradeQuery:GetBenchCraftAvailability(strippedItem))

local availability, craftState = tradeQuery:GetBenchCraftAvailability(item)
assert.is_nil(availability)
assert.are.same({ count = 1, limit = 1 }, craftState)

local replacementCraft = { type = "Suffix", group = "Strength",
modTags = { "attribute" }, types = { Ring = true }, "+(21-25) to Strength" }
tradeQuery.tradeQueryGenerator = new("TradeQueryGenerator"):TradeQueryGenerator({ itemsTab = { } })
tradeQuery.itemsTab.build = { data = { masterMods = { replacementCraft } } }
tradeQuery.statSortSelectionList = { { stat = "Life", weightMult = 1 } }
tradeQuery.slotTables[1] = { slotName = "Ring 1", considerBenchCraft = true }
tradeQuery.resultTbl[1] = { fetchedItems[1] }
local evaluation = tradeQuery:GetResultEvaluation(1, 1, function(args)
local raw = args.repItem:BuildRaw()
local hasStrengthCraft = raw:find("{crafted}", 1, true) and raw:find("to Strength", 1, true)
return { Life = hasStrengthCraft and 150 or 100 }
end, { Life = 100 })[1]

assert.is_truthy(evaluation.benchCraft:find("to Strength", 1, true))
assert.is_truthy(evaluation.benchCraftReplaced:find("to Dexterity/10% increased Rarity", 1, true))
assert.is_nil(evaluation.benchCraftItemString:find("+20 to Dexterity", 1, true))
assert.is_nil(evaluation.benchCraftItemString:find("10% increased Rarity", 1, true))
local replacementItem = new("Item"):Item(evaluation.benchCraftItemString)
local fracturedManaLine
for _, modLine in ipairs(replacementItem.explicitModLines) do
if modLine.line == "+25 to maximum Mana" then
fracturedManaLine = modLine
break
end
end
assert.is_table(fracturedManaLine)
assert.is_true(fracturedManaLine.fractured)
end)

it("preserves fetched fractured suffix values in a bench craft preview", function()
local function apiMod(description, hash, tier, domain, flags)
return { description = description, domain = domain or "explicit", hash = "stat." .. hash,
flags = flags, mods = { { name = "Test Affix", tier = tier, level = 30 } } }
end
local response = dkjson.encode({ result = { {
id = "helical-preview", listing = { price = { amount = 1, currency = "chaos", type = "~price" }, whisper = "hi", account = { name = "seller" } },
item = { rarity = "Rare", name = "Preview Subject", typeLine = "Helical Ring", implicitMods = {
{ description = "-2 Prefix Modifiers allowed" }, { description = "+1 Suffix Modifier allowed" },
{ description = "Implicit Modifiers Cannot Be Changed" }, { description = "50% increased Suffix Modifier magnitudes" },
}, explicitMods = {
apiMod("+50 to maximum Life", "explicit.life", "P2"),
apiMod("+24% to all Elemental Resistances", "explicit.all-res", "S3"),
apiMod("+69% to Lightning Resistance", "explicit.lightning-res", "S3"),
apiMod("+37% to Global Critical Strike Multiplier", "fractured.crit", "S3", "fractured", { fractured = true }),
}, extended = { hashes = { explicit = { { "explicit.life", { 0 } }, { "explicit.all-res", { 1 } }, { "explicit.lightning-res", { 2 } } }, fractured = { { "fractured.crit", { 0 } } } } } },
} } })
requests.requestQueue.fetch = { }
local fetchedItems
requests:FetchResultBlock("test", function(items) fetchedItems = items end)
table.remove(requests.requestQueue.fetch, 1).callback(response)
local sourceItem = new("Item"):Item(fetchedItems[1].item_string)
local function findLine(item, text)
for _, modLine in ipairs(item.explicitModLines) do
if modLine.line == text then return modLine end
end
end
local function assertDisplayedValue(item, line, stored, displayed)
local modLine = assert(findLine(item, line))
assert.are.equal(stored, modLine.line)
assert.is_truthy(itemLib.formatModLine(modLine):find(displayed, 1, true))
return modLine
end
local sourceCrit = assertDisplayedValue(sourceItem, "+37% to Global Critical Strike Multiplier", "+37% to Global Critical Strike Multiplier", "+37%")
assert.is_true(sourceCrit.fractured)
assertDisplayedValue(sourceItem, "+24% to all Elemental Resistances", "+24% to all Elemental Resistances", "+24%")
assertDisplayedValue(sourceItem, "+69% to Lightning Resistance", "+69% to Lightning Resistance", "+69%")

local tradeQuery = new("TradeQuery"):TradeQuery({ itemsTab = { } })
local strengthCraft = { type = "Suffix", group = "Strength", modTags = { "attribute" }, types = { Ring = true }, "+(21-25) to Strength" }
tradeQuery.tradeQueryGenerator = new("TradeQueryGenerator"):TradeQueryGenerator({ itemsTab = { } })
tradeQuery.itemsTab.build = { data = { masterMods = { strengthCraft } } }
tradeQuery.statSortSelectionList = { { stat = "Life", weightMult = 1 } }
tradeQuery.slotTables[1] = { slotName = "Ring 1", considerBenchCraft = true }
tradeQuery.resultTbl[1] = { fetchedItems[1] }
local previousAffixQuality = main.defaultItemAffixQuality
main.defaultItemAffixQuality = 0.5
local evaluation = tradeQuery:GetResultEvaluation(1, 1, function(args)
return { Life = args.repItem:BuildRaw():find("to Strength", 1, true) and 150 or 100 }
end, { Life = 100 })[1]
main.defaultItemAffixQuality = previousAffixQuality
assert.is_truthy(evaluation.benchCraftItemString)
local previewItem = new("Item"):Item(evaluation.benchCraftItemString)
local previewCrit = assertDisplayedValue(previewItem, "+37% to Global Critical Strike Multiplier", "+37% to Global Critical Strike Multiplier", "+37%")
assert.is_true(previewCrit.fractured)
assertDisplayedValue(previewItem, "+24% to all Elemental Resistances", "+24% to all Elemental Resistances", "+24%")
assertDisplayedValue(previewItem, "+69% to Lightning Resistance", "+69% to Lightning Resistance", "+69%")
local previewStrength = assert(findLine(previewItem, "+(21-25) to Strength"))
assert.is_true(previewStrength.crafted)
assert.is_truthy(itemLib.formatModLine(previewStrength):find("+34 to Strength", 1, true))
end)
end)

describe("FetchResults", function()
Expand Down
Loading
Loading