Skip to content
276 changes: 276 additions & 0 deletions spec/System/TestTradeQueryGenerator_spec.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
describe("TradeQueryGenerator", function()
local mock_queryGen = new("TradeQueryGenerator"):TradeQueryGenerator({ itemsTab = {} })
local tradeResistanceGrouping = LoadModule("Classes/TradeResistanceGrouping")

describe("ProcessMod", function()
-- Pass: Mod line maps correctly to trade stat entry without error
Expand Down Expand Up @@ -153,6 +154,281 @@ describe("TradeQueryGenerator", function()
end)
end)

describe("resistance search options", function()
it("derives non-negative cap shortfalls from the blank-item output", function()
assert.are.same({ Fire = 12, Cold = 0, Lightning = 34, Chaos = 56 },
tradeResistanceGrouping.getResistanceCapShortfallByType({
MissingFireResist = 12,
MissingColdResist = -3,
MissingLightningResist = 34,
MissingChaosResist = 56,
}))
end)

it("annotates weights through the real GenerateModWeights method", function()
local queryGen = new("TradeQueryGenerator"):TradeQueryGenerator({ itemsTab = {} })
queryGen.modWeights = {}
queryGen.alreadyWeightedMods = {}
queryGen.calcContext = {
itemCategory = "Ring",
testItem = new("Item"):Item("Rarity: RARE\nTest Ring\nCoral Ring\nImplicits: 0"),
baseOutput = { Life = 100 },
baseStatValue = 1000,
calcFunc = function() return { Life = 110 } end,
options = {
includeTalisman = false,
statWeights = { { stat = "Life", weightMult = 1 } },
},
slot = { slotName = "Ring 1" },
}
queryGen:GenerateModWeights({
fireResistance = {
Ring = { min = 10, max = 10, subType = "" },
tradeMod = { id = "explicit.fire_resistance", text = "+#% to Fire Resistance" },
specialCaseData = {},
},
})

assert.are.equal(1, #queryGen.modWeights)
assert.is_true(queryGen.modWeights[1].resistTag.elemental)
assert.are.equal(queryGen.modWeights[1].weight, queryGen.modWeights[1].normalisedWeight)
end)

local function finishQuery(options, weights)
options = options or {}
local queryGen = new("TradeQueryGenerator"):TradeQueryGenerator({ itemsTab = {} })
queryGen.tradeTypeIndex = 4
queryGen.modWeights = weights
queryGen.calcContext = {
itemCategoryQueryStr = "accessory.ring",
special = {},
testItem = new("Item"):Item("Rarity: RARE\nTest Ring\nCoral Ring\nImplicits: 0"),
baseOutput = { Life = 100 },
baseStatValue = 1000,
calcFunc = function() return { Life = 100 } end,
options = {
includeMirrored = true, influence1 = 1, influence2 = 1,
statWeights = { { stat = "Life", weightMult = 1 } },
includeResistSwaps = options.includeResistSwaps,
includeResistCaps = options.includeResistCaps,
},
requiredMods = options.requiredMods or {},
resistanceCapShortfallByType = options.resistanceCapShortfallByType,
}
queryGen.requesterContext = { slotTbl = { sentinel = true } }
local queryJson
local queryOptions
local queryError
queryGen.requesterCallback = function(_, json, errMsg, optionsSnapshot)
queryJson = json
queryError = errMsg
queryOptions = optionsSnapshot
end
queryGen:FinishQuery()
return require("dkjson").decode(queryJson), queryGen.requesterContext.slotTbl, queryOptions, queryError
end

local function annotatedWeight(id, text, weight, meanStatDiff)
return tradeResistanceGrouping.annotateResistanceWeight({
tradeModId = id, weight = weight, meanStatDiff = meanStatDiff, invert = false,
}, text)
end

local function weight(id, value, meanStatDiff)
return { tradeModId = id, weight = value, meanStatDiff = meanStatDiff or value, invert = false }
end

local function filtersById(query, groupType)
local filters = { }
for _, group in ipairs(query.query.stats) do
if not groupType or group.type == groupType then
for _, filter in ipairs(group.filters) do
filters[filter.id] = filter
end
end
end
return filters
end

local function minimumsById(query)
local minimums = { }
for id, filter in pairs(filtersById(query, "and")) do
minimums[id] = filter.value.min
end
return minimums
end

it("groups resistance without changing damage filters", function()
local query = finishQuery({ includeResistSwaps = true }, {
annotatedWeight("explicit.fire_resistance", "+#% to Fire Resistance", 10, 10),
weight("explicit.fire_damage", 8),
weight("explicit.life", 6),
})
local filters = filtersById(query, "weight")

assert.is_not_nil(filters["pseudo.pseudo_total_elemental_resistance"])
assert.is_not_nil(filters["explicit.fire_damage"])
assert.is_not_nil(filters["explicit.life"])
assert.is_nil(filters["explicit.fire_resistance"])
end)

it("leaves non-swappable resistance filters unchanged", function()
local cases = {
{ "hybrid elemental and chaos", "explicit.hybrid_resistance", "+#% to Fire and Chaos Resistances" },
{ "implicit resistance", "implicit.fire_resistance", "+#% to Fire Resistance" },
}
for _, case in ipairs(cases) do
local query = finishQuery({ includeResistSwaps = true }, { annotatedWeight(case[2], case[3], 10, 10) })
local filters = query.query.stats[1].filters
assert.are.equal(1, #filters, case[1])
assert.are.equal(case[2], filters[1].id, case[1])
end
end)

it("does not let hybrid resistance expansion evict a lower-priority filter", function()
local weights = {
annotatedWeight("explicit.hybrid_resistance", "+#% to Fire and Chaos Resistances", 100, 100),
}
for index = 1, 31 do
table.insert(weights, weight(string.format("explicit.filler_%d", index), 100 - index))
end
table.insert(weights, weight("explicit.low_priority_filter", 1))

local query = finishQuery({ includeResistSwaps = true }, weights)
local filters = filtersById(query, "weight")

assert.are.equal(33, #query.query.stats[1].filters)
assert.is_not_nil(filters["explicit.hybrid_resistance"])
assert.is_not_nil(filters["explicit.low_priority_filter"])
end)

it("does not persist the swap option into requester context", function()
local _, slotTable, queryOptions = finishQuery({ includeResistSwaps = true }, {
weight("explicit.life", 6),
})

assert.are.same({ sentinel = true }, slotTable)
assert.are.same({ includeResistSwaps = true, includeResistCaps = false, weightAdjustedSearch = true }, queryOptions)
end)

it("normalises multi-element resistance weights before pseudo grouping", function()
local query = finishQuery({ includeResistSwaps = true }, {
annotatedWeight("explicit.all_resistance", "+#% to all Elemental Resistances", 30, 30),
annotatedWeight("explicit.fire_resistance", "+#% to Fire Resistance", 8, 8),
})
local filter = query.query.stats[1].filters[1]

assert.are.equal("pseudo.pseudo_total_elemental_resistance", filter.id)
assert.are.equal(10, filter.value.weight)
end)

it("uses individual or grouped cap minimums according to the swap option", function()
local shortfalls = { Fire = 10, Cold = 20, Lightning = 30, Chaos = 40 }
local cases = {
{ label = "caps only", options = { includeResistCaps = true }, weights = {
annotatedWeight("explicit.fire_resistance", "+#% to Fire Resistance", 10, 10),
annotatedWeight("implicit.cold_resistance", "+#% to Cold Resistance", 9, 9),
annotatedWeight("explicit.fire_chaos_resistance", "+#% to Fire and Chaos Resistances", 8, 8),
weight("explicit.life", 6),
}, minimums = {
["pseudo.pseudo_total_fire_resistance"] = 10,
["pseudo.pseudo_total_cold_resistance"] = 20,
["pseudo.pseudo_total_lightning_resistance"] = 30,
["pseudo.pseudo_total_chaos_resistance"] = 40,
} },
{ label = "caps with swaps", options = { includeResistCaps = true, includeResistSwaps = true }, weights = {
annotatedWeight("explicit.fire_resistance", "+#% to Fire Resistance", 10, 10),
weight("explicit.life", 6),
}, minimums = {
["pseudo.pseudo_total_elemental_resistance"] = 60,
["pseudo.pseudo_total_chaos_resistance"] = 40,
} },
}
for _, case in ipairs(cases) do
case.options.resistanceCapShortfallByType = shortfalls
local query, _, queryOptions = finishQuery(case.options, case.weights)
assert.are.same(case.minimums, minimumsById(query), case.label)
assert.are.equal(1, #query.query.stats[1].filters, case.label)
assert.are.equal("explicit.life", query.query.stats[1].filters[1].id, case.label)
assert.are.equal(0, query.query.stats[1].value.min, case.label)
assert.is_false(queryOptions.weightAdjustedSearch, case.label)
end
end)

it("builds an AND-only price-sorted query when caps remove every weighted filter", function()
local query, _, queryOptions = finishQuery({
includeResistCaps = true,
resistanceCapShortfallByType = { Fire = 25 },
}, {
annotatedWeight("explicit.fire_resistance", "+#% to Fire Resistance", 10, 10),
})

assert.are.equal(1, #query.query.stats)
assert.are.equal("and", query.query.stats[1].type)
assert.are.same({ price = "asc" }, query.sort)
assert.is_false(queryOptions.weightAdjustedSearch)
end)

it("does not add zero resistance minimums or an empty AND group", function()
local query, _, _, queryError = finishQuery({
includeResistCaps = true,
resistanceCapShortfallByType = { Fire = 0, Cold = 0, Lightning = 0, Chaos = 0 },
}, {
annotatedWeight("explicit.fire_resistance", "+#% to Fire Resistance", 10, 10),
})

assert.are.equal(0, #query.query.stats)
assert.is_truthy(queryError)
end)

it("preserves the upstream weighted-group error for required-only searches when caps are off", function()
local query, _, queryOptions, queryError = finishQuery({
requiredMods = { { tradeId = "explicit.required", value = 10 } },
}, {})

assert.are.equal("weight", query.query.stats[1].type)
assert.are.equal(0, #query.query.stats[1].filters)
assert.are.equal("and", query.query.stats[2].type)
assert.are.same({ ["statgroup.0"] = "desc" }, query.sort)
assert.is_false(queryOptions.weightAdjustedSearch)
assert.is_truthy(queryError)
end)

it("budgets cap and required filters before weighted filters", function()
local requiredMods = {}
for index = 1, 32 do
requiredMods[index] = { tradeId = "explicit.required_" .. index, value = index }
end
local query, _, queryOptions = finishQuery({
includeResistCaps = true,
resistanceCapShortfallByType = { Fire = 25 },
requiredMods = requiredMods,
}, {
weight("explicit.life", 6),
})
local filterCount = 0
for _, group in ipairs(query.query.stats) do
filterCount = filterCount + #group.filters
end

assert.are.equal(34, filterCount)
assert.is_false(queryOptions.weightAdjustedSearch)
end)

it("preserves upstream filter order when resistance swaps are disabled", function()
local query = finishQuery({ includeResistSwaps = false }, {
annotatedWeight("explicit.fire_resistance", "+#% to Fire Resistance", 3, 30),
weight("explicit.fire_damage", 2, 20),
weight("explicit.life", 1, 10),
})
local filters = query.query.stats[1].filters

assert.are.equal("explicit.fire_resistance", filters[1].id)
assert.are.equal("explicit.fire_damage", filters[2].id)
assert.are.equal("explicit.life", filters[3].id)
end)
end)

describe("Filter prioritization", function()
it("counts socket and link constraints against MAX_FILTERS", function()
local queryGen = new("TradeQueryGenerator"):TradeQueryGenerator({ itemsTab = { items = {} } })
Expand Down
Loading
Loading