Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
dd9af08
feat(weighted-score): add shared WeightedScore module and integrate i…
mcagnion Mar 21, 2026
1c22786
test(weighted-score): add WeightedScore test coverage
mcagnion Mar 21, 2026
76da1ab
feat(weighted-score): add WeightedScore sort support to anoint panel
mcagnion Mar 22, 2026
d131886
feat(weighted-score): add getValue to WeightedScore powerStatList entry
mcagnion Mar 22, 2026
7269245
fix(weighted-score): cache WeightedScore module load in Data.lua
mcagnion Mar 23, 2026
693a2a2
fix(weighted-score): support getValue in generateFallbackWeights
mcagnion Mar 24, 2026
22ba8ed
fix(weighted-score): propagate getValue into fallbackWeightsList entries
mcagnion Mar 24, 2026
f658951
fix(trade): initialize stat weights before opening editor
mcagnion Mar 28, 2026
d5dd43c
feat(weighted-score): show weighted score in Power Report
mcagnion May 9, 2026
78a581f
refactor(weighted-score): unify Edit Weights affordance via dropdown …
mcagnion May 10, 2026
6abb446
fix(weighted-score): preserve output stat semantics
mcagnion Jul 26, 2026
4d6aa30
fix(weighted-score): limit sorting to supported surfaces
mcagnion Jul 26, 2026
4d49bf9
fix(weighted-score): preserve Item DB ranking
mcagnion Jul 26, 2026
bbe974e
fix(weighted-score): preserve comparison context
mcagnion Jul 26, 2026
699de1f
test(weighted-score): cover combined stat weights
mcagnion Jul 26, 2026
31fe642
refactor(weighted-score): share default weights
mcagnion Jul 26, 2026
6ab3c64
fix(weighted-score): use Full DPS for anoint ranking
mcagnion Jul 26, 2026
86a3bc4
fix(weighted-score): persist weight edits
mcagnion Jul 26, 2026
0e9c4e1
fix(weighted-score): place score last in menus
mcagnion Jul 28, 2026
3b093e2
fix(weighted-score): place weight editor after score
mcagnion Jul 28, 2026
3f90e45
feat(weighted-score): sort item modifiers by score
mcagnion Jul 28, 2026
5f40766
fix(weighted-score): preserve refresh callback after reset
mcagnion Aug 6, 2026
fc8f530
Fix weighted score sorting for crafted affixes
mcagnion Aug 6, 2026
37efced
Add weighted score editing to remaining selectors
mcagnion Aug 7, 2026
c7fe989
Adapt weighted score tests to current class syntax
mcagnion Aug 15, 2026
051f4cb
Simplify weighted score consumer plumbing
mcagnion Aug 20, 2026
b016206
Clarify weighted score calculation semantics
mcagnion Aug 20, 2026
fc88c9b
Merge origin/dev into weighted score
mcagnion Aug 20, 2026
fb17904
Centralize weighted score editor routing
mcagnion Aug 20, 2026
f6367b4
Compact weighted score test contracts
mcagnion Aug 20, 2026
62dbdf6
Extract Power Report fix from weighted score
mcagnion Aug 21, 2026
4474ae4
Clarify weighted score semantic contracts
mcagnion Aug 21, 2026
79fb1a6
Fix contextual weighted score references
mcagnion Aug 21, 2026
0c9f12f
Compact weighted score test contracts
mcagnion Aug 23, 2026
d68cdf3
Preserve upstream power report filtering
mcagnion Aug 23, 2026
da4e8a3
Strengthen Weighted Score FullDPS fallback test
mcagnion Aug 28, 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