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
78b6aeb
Add shared Weighted Score ranking
mcagnion Mar 21, 2026
7323da4
Test shared Weighted Score behavior
mcagnion Mar 21, 2026
0809657
Add Weighted Score sorting to Anoint
mcagnion Mar 22, 2026
4477fc5
Expose Weighted Score through powerStatList
mcagnion Mar 22, 2026
bebef3b
Load WeightedScore once in Data
mcagnion Mar 23, 2026
bc8c53b
Use Weighted Score for Timeless Jewel fallback weights
mcagnion Mar 24, 2026
70e3d8b
Keep getValue in fallback weight entries
mcagnion Mar 24, 2026
13dc625
Initialize stat weights before opening the editor
mcagnion Mar 28, 2026
664226c
Show Weighted Score in Power Report
mcagnion May 9, 2026
9b13712
Put Edit Weights next to Weighted Score
mcagnion May 10, 2026
9360b62
Preserve output stat semantics for Weighted Score
mcagnion Jul 26, 2026
fcfee41
Limit Weighted Score to supported sort menus
mcagnion Jul 26, 2026
4defe17
Preserve Item DB ranking for Weighted Score
mcagnion Jul 26, 2026
0534023
Preserve comparison context for Weighted Score
mcagnion Jul 26, 2026
5b224f3
Test combined Weighted Score inputs
mcagnion Jul 26, 2026
9489598
Share the default Weighted Score weights
mcagnion Jul 26, 2026
fbd25a6
Use Full DPS for Anoint ranking
mcagnion Jul 26, 2026
2c1d063
Save Weighted Score weight edits
mcagnion Jul 26, 2026
962c111
Put Weighted Score last in menus
mcagnion Jul 28, 2026
5d8b4f7
Put Edit Weights after Weighted Score
mcagnion Jul 28, 2026
a7a9263
Sort item modifiers by Weighted Score
mcagnion Jul 28, 2026
ec21b84
Preserve the refresh callback after Reset
mcagnion Aug 6, 2026
beedd99
Fix weighted score sorting for crafted affixes
mcagnion Aug 6, 2026
f3396f2
Add weighted score editing to remaining selectors
mcagnion Aug 7, 2026
0385cb7
Adapt weighted score tests to current class syntax
mcagnion Aug 15, 2026
fcbb83f
Simplify weighted score consumer plumbing
mcagnion Aug 20, 2026
e8447b6
Clarify weighted score calculation semantics
mcagnion Aug 20, 2026
42f5eab
Centralize weighted score editor routing
mcagnion Aug 20, 2026
995240a
Compact weighted score test contracts
mcagnion Aug 20, 2026
06b7e8e
Extract Power Report fix from weighted score
mcagnion Aug 21, 2026
2dca150
Clarify weighted score semantic contracts
mcagnion Aug 21, 2026
35d8eb2
Fix contextual weighted score references
mcagnion Aug 21, 2026
e0c6986
Compact weighted score test contracts
mcagnion Aug 23, 2026
dfb31ff
Preserve upstream power report filtering
mcagnion Aug 23, 2026
aeb8f7e
Strengthen Weighted Score FullDPS fallback test
mcagnion Aug 28, 2026
ddd2a86
Check weighted scores exactly for the current build
mcagnion Sep 27, 2026
53ece6f
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"] = {} },
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 @@ -176,4 +176,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