Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
27b8e5e
Add shared Weighted Score ranking
mcagnion Mar 21, 2026
e65816a
Test shared Weighted Score behavior
mcagnion Mar 21, 2026
26ba4d9
Add Weighted Score sorting to Anoint
mcagnion Mar 22, 2026
5ccf76d
Expose Weighted Score through powerStatList
mcagnion Mar 22, 2026
399e328
Load WeightedScore once in Data
mcagnion Mar 23, 2026
f503184
Use Weighted Score for Timeless Jewel fallback weights
mcagnion Mar 24, 2026
3884b69
Keep getValue in fallback weight entries
mcagnion Mar 24, 2026
ed08a34
Initialize stat weights before opening the editor
mcagnion Mar 28, 2026
69cbcef
Show Weighted Score in Power Report
mcagnion May 9, 2026
7b31c4d
Put Edit Weights next to Weighted Score
mcagnion May 10, 2026
a0cb340
Preserve output stat semantics for Weighted Score
mcagnion Jul 26, 2026
b7787f0
Limit Weighted Score to supported sort menus
mcagnion Jul 26, 2026
43f79fd
Preserve Item DB ranking for Weighted Score
mcagnion Jul 26, 2026
6be9a59
Preserve comparison context for Weighted Score
mcagnion Jul 26, 2026
057cd5d
Test combined Weighted Score inputs
mcagnion Jul 26, 2026
45d58c7
Share the default Weighted Score weights
mcagnion Jul 26, 2026
151229e
Use Full DPS for Anoint ranking
mcagnion Jul 26, 2026
298e15b
Save Weighted Score weight edits
mcagnion Jul 26, 2026
5c12c5a
Put Weighted Score last in menus
mcagnion Jul 28, 2026
8da9862
Put Edit Weights after Weighted Score
mcagnion Jul 28, 2026
e4cdcd1
Sort item modifiers by Weighted Score
mcagnion Jul 28, 2026
04228f0
Preserve the refresh callback after Reset
mcagnion Aug 6, 2026
7b90784
Fix weighted score sorting for crafted affixes
mcagnion Aug 6, 2026
08391a2
Add weighted score editing to remaining selectors
mcagnion Aug 7, 2026
882ec3c
Adapt weighted score tests to current class syntax
mcagnion Aug 15, 2026
cddc52f
Simplify weighted score consumer plumbing
mcagnion Aug 20, 2026
0accadb
Clarify weighted score calculation semantics
mcagnion Aug 20, 2026
253fb8a
Centralize weighted score editor routing
mcagnion Aug 20, 2026
a9aad64
Compact weighted score test contracts
mcagnion Aug 20, 2026
2d2d32c
Extract Power Report fix from weighted score
mcagnion Aug 21, 2026
35dd8e6
Clarify weighted score semantic contracts
mcagnion Aug 21, 2026
3b6a0a6
Fix contextual weighted score references
mcagnion Aug 21, 2026
f96c64a
Compact weighted score test contracts
mcagnion Aug 23, 2026
c830506
Preserve upstream power report filtering
mcagnion Aug 23, 2026
27cada4
Strengthen Weighted Score FullDPS fallback test
mcagnion Aug 28, 2026
77e4d3a
Check weighted scores exactly for the current build
mcagnion Sep 27, 2026
ae39701
Keep Weighted Score sorting in Add Implicit
mcagnion Sep 27, 2026
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
59 changes: 59 additions & 0 deletions spec/System/TestAbyssTimelessJewel_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,65 @@ describe("Abyss timeless jewels", function()
assert.matches("abyss_special_small_attribute25, 1, 0, 0", build.timelessData.searchListFallback, nil, true)
end)

it("generates useful non-uniform fallback weights with Weighted Score", function()
build.skillsTab:PasteSocketGroup("Ethereal Knives 20/0 1\n")
runCallback("OnFrame")
local socketId
for id, node in pairs(build.spec.nodes) do
if node.isJewelSocket and node.name ~= "Charm Socket" then
socketId = id
break
end
end
assert.is_truthy(socketId, "fixture requires a passive-tree jewel socket")

build.timelessData.jewelType = { id = 11 }
build.timelessData.conquerorType = { }
build.timelessData.jewelSocket = { id = socketId }
build.itemsTab.tradeQuery.statSortSelectionList = {
{ stat = "FullDPS", label = "Full DPS", weightMult = 1 },
{ stat = "TotalEHP", label = "Effective Hit Pool", weightMult = 0.5 },
}
build.treeTab:FindTimelessJewel()
local controls = main.popups[1].controls
controls.fallbackWeightsList:SelByValue("WeightedScore", "stat")
assert.are.equal("WeightedScore", controls.fallbackWeightsList:GetSelValue().stat)

local originalGetMiscCalculator = build.calcsTab.GetMiscCalculator
local requestedFullDPS = { }
build.calcsTab.GetMiscCalculator = function(self, ...)
local calcFunc, calcBase = originalGetMiscCalculator(self, ...)
return function(params, useFullDPS)
requestedFullDPS[#requestedFullDPS + 1] = useFullDPS
return calcFunc(params, useFullDPS)
end, calcBase
end
local ok, errMsg = pcall(controls.fallbackWeightsButton.onClick)
build.calcsTab.GetMiscCalculator = originalGetMiscCalculator
assert.is_true(ok, errMsg)
assert.is_true(#requestedFullDPS > 1, "Generate must evaluate a baseline and candidates")
for _, useFullDPS in ipairs(requestedFullDPS) do
assert.is_true(useFullDPS)
end

local distinctWeights = { }
local usefulWeightCount = 0
for line in build.timelessData.searchListFallback:gmatch("[^\r\n]+") do
local weight1, weight2 = line:match("^[^,]+,%s*([^,]+),%s*([^,]+)")
weight1, weight2 = tonumber(weight1), tonumber(weight2)
if weight1 and weight2 and math.abs(weight1) + math.abs(weight2) > 0 then
usefulWeightCount = usefulWeightCount + 1
distinctWeights[weight1 .. "," .. weight2] = true
end
end
local distinctWeightCount = 0
for _ in pairs(distinctWeights) do
distinctWeightCount = distinctWeightCount + 1
end
assert.is_true(usefulWeightCount > 0, "Generate must produce at least one useful fallback weight")
assert.is_true(distinctWeightCount > 1, "Weighted Score must not collapse every node to one weight")
end)

it("reads Zorath seed 6564 node and Inquisitor ascendancy changes", function()
data.timelessJewelLUTs[11] = parseAbyssJewel(11, zorathExampleData())
local expected = {
Expand Down
126 changes: 83 additions & 43 deletions spec/System/TestItemDBControl_spec.lua
Original file line number Diff line number Diff line change
@@ -1,65 +1,105 @@
describe("ItemDBControl", function()
local originalGetCursorPos

before_each(function()
originalGetCursorPos = GetCursorPos
end)

after_each(function()
GetCursorPos = originalGetCursorPos
end)

it("sorts lower-is-better stats below zero", function()
local function makeItem(name)
return {
name = name,
base = {},
enchantModLines = {},
implicitModLines = {},
explicitModLines = {},
baseModList = {},
}
local function findPowerStat(statName)
for _, stat in ipairs(data.powerStatList) do
if stat.stat == statName then
return stat
end
end
local betterItem = makeItem("Better Item")
local worseItem = makeItem("Worse Item")
local invalidItem = makeItem("Invalid Item")
local takenDamage = {
[betterItem] = 80,
[worseItem] = 120,
end
local function newItem(name)
return {
name = name,
base = {},
enchantModLines = {},
implicitModLines = {},
explicitModLines = {},
baseModList = {},
}
end
local function newRankingFixture(weights)
local items = {
better = newItem("Better Item"),
worse = newItem("Worse Item"),
invalid = newItem("Invalid Item"),
}
local values = {
[items.better] = { PhysicalTakenHit = 80, FullDPS = 120 },
[items.worse] = { PhysicalTakenHit = 120, FullDPS = 80 },
}
local requestedFullDPS = { }
local itemsTab = {
activeItemSet = { useSecondWeaponSet = false },
slots = { ["Body Armour"] = { shown = function() return true end } },
build = {
calcsTab = {
GetMiscCalculator = function()
return function(args)
return { PhysicalTakenHit = takenDamage[args.repItem] }
end
end,
},
},
tradeQuery = { statSortSelectionList = weights or {} },
IsItemValidForSlot = function(_, item)
return item ~= invalidItem
return item ~= items.invalid
end,
}
itemsTab.build = {
itemsTab = itemsTab,
calcsTab = {
GetMiscCalculator = function()
return function(args, useFullDPS)
requestedFullDPS[#requestedFullDPS + 1] = useFullDPS
return values[args.repItem]
end, { PhysicalTakenHit = 100, FullDPS = 100 }
end,
},
}
local control = new("ItemDBControl"):ItemDBControl(nil, { 0, 0, 100, 100 }, itemsTab, {
list = { invalidItem, betterItem, worseItem },
list = { items.invalid, items.better, items.worse },
}, "RARE")
control.sortOrder = { control.sortControl.STAT, control.sortControl.NAME }
return control, items, requestedFullDPS
end

before_each(function()
originalGetCursorPos = GetCursorPos
end)

after_each(function()
GetCursorPos = originalGetCursorPos
end)

it("sorts lower-is-better stats below zero", function()
local control, items = newRankingFixture()
control.sortDetail = {
stat = "PhysicalTakenHit",
transform = function(value) return -value end,
}
control.sortOrder = { control.sortControl.STAT, control.sortControl.NAME }

control:ListBuilder()

assert.are.equal(betterItem, control.list[1])
assert.are.equal(worseItem, control.list[2])
assert.are.equal(invalidItem, control.list[3])
assert.are.equal(-80, betterItem.measuredPower)
assert.are.equal(-120, worseItem.measuredPower)
assert.are.equal(-math.huge, invalidItem.measuredPower)
assert.are.same({ items.better, items.worse, items.invalid }, control.list)
assert.are.equal(-80, items.better.measuredPower)
assert.are.equal(-120, items.worse.measuredPower)
assert.are.equal(-math.huge, items.invalid.measuredPower)
end)

it("preserves negative WeightedScore results and skips unneeded FullDPS", function()
local weights = {
{ stat = "PhysicalTakenHit", weightMult = 1, transform = function(value) return -value end },
}
local control, items, requestedFullDPS = newRankingFixture(weights)
control.sortDetail = copyTable(findPowerStat("WeightedScore"))

control:ListBuilder()

assert.are.same({ items.better, items.worse, items.invalid }, control.list)
assert.is_true(items.better.measuredPower < 0)
assert.is_true(items.worse.measuredPower < items.better.measuredPower)
assert.are.equal(-math.huge, items.invalid.measuredPower)
assert.are.same({ false, false }, requestedFullDPS)
end)

it("requests FullDPS for WeightedScore when active weights need it", function()
local control, _, requestedFullDPS = newRankingFixture({ { stat = "FullDPS", weightMult = 1 } })
control.sortDetail = copyTable(findPowerStat("WeightedScore"))

control:ListBuilder()

assert.are.same({ true, true }, requestedFullDPS)
end)

it("searches Foulborn modifier text without case sensitivity", function()
Expand Down
67 changes: 67 additions & 0 deletions spec/System/TestNotableDBControl_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
describe("NotableDBControl", function()
local function findPowerStat(statName)
for _, stat in ipairs(data.powerStatList) do
if stat.stat == statName then
return copyTable(stat)
end
end
end
local function newAnointFixture(weights, includeSecondCandidate)
local candidates = {
{ dn = "Candidate A", sd = {}, recipe = { "Fixture Oil" }, modKey = "CandidateA" },
{ dn = "Candidate B", sd = {}, recipe = { "Fixture Oil" }, modKey = "CandidateB" },
}
local displayItem = { base = { type = "Amulet" } }
local outputs = {
[displayItem] = { FullDPS = 200, TotalEHP = 100 },
[candidates[1]] = { FullDPS = 180, TotalEHP = 120 },
[candidates[2]] = { FullDPS = 150, TotalEHP = 140 },
}
local requestedFullDPS = { }
local itemsTab = {
displayItem = displayItem,
tradeQuery = { statSortSelectionList = weights or {} },
anointItem = function(_, node) return node end,
}
itemsTab.build = {
itemsTab = itemsTab,
calcsTab = {
GetMiscCalculator = function()
return function(args, useFullDPS)
requestedFullDPS[#requestedFullDPS + 1] = useFullDPS
return outputs[args.repItem] or { FullDPS = 100, TotalEHP = 100 }
end
end,
},
}
local list = includeSecondCandidate and candidates or { candidates[1] }
local control = new("NotableDBControl"):NotableDBControl(nil, { 0, 0, 100, 100 }, itemsTab, list, "ANOINT")
control.sortOrder = { control.sortControl.STAT, control.sortControl.NAME }
return control, candidates, requestedFullDPS
end

it("sorts WeightedScore anoints against the displayed item", function()
local weights = {
{ stat = "FullDPS", weightMult = 1 },
{ stat = "TotalEHP", weightMult = 1 },
}
local control, candidates, requestedFullDPS = newAnointFixture(weights, true)
control.sortDetail = findPowerStat("WeightedScore")

control:ListBuilder()

assert.are.same({ true, true, true }, requestedFullDPS)
assert.are.same({ candidates[2], candidates[1] }, control.list)
assert.is_true(candidates[2].measuredPower > candidates[1].measuredPower)
assert.are.equal(candidates[2].measuredPower, control.sortMaxPower)
end)

it("keeps scalar anoint impact relative to the item without an anoint", function()
local control, candidates = newAnointFixture()
control.sortDetail = findPowerStat("FullDPS")

control:ListBuilder()

assert.are.equal(80, candidates[1].measuredPower)
end)
end)
46 changes: 46 additions & 0 deletions spec/System/TestTradeQuery_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -260,4 +260,50 @@ describe("TradeQuery", function()
assert.are.equals(1.2, result)
end)
end)

describe("SetStatWeights", function()
local capturedControls
local originalOpenPopup
local originalClosePopup
before_each(function()
originalOpenPopup = main.OpenPopup
originalClosePopup = main.ClosePopup
main.OpenPopup = function(_, _, _, _, controls)
capturedControls = controls
end
main.ClosePopup = function() end
end)
after_each(function()
main.OpenPopup = originalOpenPopup
main.ClosePopup = originalClosePopup
end)

it("marks the build modified after saving changed weights", function()
local itemsTab = {}
local tradeQuery = new("TradeQuery"):TradeQuery(itemsTab)
tradeQuery:SetStatWeights()
for _, entry in ipairs(capturedControls.ListControl.list) do
if entry.stat.stat == "FullDPS" then
entry.stat.weightMult = 0.75
break
end
end
capturedControls.finalise.onClick()

assert.is_true(itemsTab.modFlag)
assert.are.equal(0.75, tradeQuery.statSortSelectionList[1].weightMult)
end)

it("preserves the save callback after resetting weights", function()
local callbackCount = 0
local tradeQuery = new("TradeQuery"):TradeQuery({})
tradeQuery:SetStatWeights(nil, function()
callbackCount = callbackCount + 1
end)
capturedControls.reset.onClick()
capturedControls.finalise.onClick()

assert.are.equal(1, callbackCount)
end)
end)
end)
Loading
Loading