Skip to content
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)
82 changes: 82 additions & 0 deletions spec/System/TestTradeQueryRequests_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,88 @@ 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 and crafted 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"),
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 } } },
} },
},
} },
})
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 }, { modLines[1].prefix, modLines[2].prefix })
assert.are.equal(modLines[1].modGroup, modLines[2].modGroup)
assert.are.same({ true, true, true, true },
{ modLines[3].suffix, modLines[4].suffix, modLines[5].suffix, modLines[6].suffix })
assert.are_not.equal(modLines[3].modGroup, modLines[4].modGroup)
assert.are.same({ true, true }, { modLines[5].crafted, modLines[6].crafted })
assert.are.equal(modLines[5].modGroup, modLines[6].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 = 2, 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))
end)
end)

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