Skip to content

Commit 80095bd

Browse files
committed
refactor: code quality cleanup after review
- Extract normalizeModText() helper for repeated gsub pattern (6 uses) - Lift qualityModifierToCatalyst to module level, built from catalystList - Cache specName regex match to avoid double-match on catalyst parsing - Defer localOverrides alloc until preferLocal is true - Consolidate Eater/Exarch lookup merge into single loop - Add nil-guard on fetchStats() in InitMods pseudo-stats path - Fix spec indentation on added lines only (no whole-file reformat) Claude Code helped here.
1 parent df97131 commit 80095bd

3 files changed

Lines changed: 111 additions & 38 deletions

File tree

spec/System/TestTradeQueryGenerator_spec.lua

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,5 +238,63 @@ describe("TradeQueryGenerator", function()
238238
assert.are.equal(1, #filters)
239239
assert.are.equal(2, filters[1].value.min)
240240
end)
241+
242+
-- Pass: Each individual stat in the count group has value.min = 1 so the trade API
243+
-- evaluates them correctly. Without this the filter silently matches everything.
244+
-- Fail: Missing value field = trade site ignores the stat, returning uncrafted items
245+
it("each stat inside the count group has value.min = 1", function()
246+
local filters = mock_queryGen:BuildCraftedSlotFilters(1, 0)
247+
assert.are.equal(1, #filters)
248+
for _, stat in ipairs(filters[1].filters) do
249+
assert.is_not_nil(stat.value, "stat missing value field: " .. tostring(stat.id))
250+
assert.are.equal(1, stat.value.min)
251+
end
252+
end)
253+
end)
254+
255+
describe("CountCraftedAffixesFromModLines", function()
256+
-- Uses the item's explicitModLines (modLine.crafted = true) rather than the affix
257+
-- slot tables, which is the only reliable source for PoB-crafted items.
258+
-- Pool entries: numeric-indexed text lines + type (string) + types (table, marks crafted).
259+
260+
-- Synthetic affix pool covering both a crafted suffix and a crafted prefix.
261+
local syntheticPool = {
262+
CraftedMana1 = { "(25-34) to maximum Mana", type = "Suffix", types = { str = true } },
263+
CraftedArmour1 = { "(30-40)% increased Armour", type = "Prefix", types = { str = true } },
264+
}
265+
266+
-- Pass: A crafted suffix mod line matches the pool and suffixCount = 1
267+
-- Fail: Count stays 0 = crafted mod not matched, no slot filter emitted
268+
it("counts a crafted suffix mod line", function()
269+
local modLines = {
270+
{ line = "+29 to maximum Mana", crafted = true },
271+
{ line = "+45 to maximum Energy Shield", crafted = false },
272+
}
273+
local result = mock_queryGen:CountCraftedAffixesFromModLines(modLines, syntheticPool, nil)
274+
assert.are.equal(0, result.prefix)
275+
assert.are.equal(1, result.suffix)
276+
end)
277+
278+
-- Pass: A crafted prefix mod line matches the pool and prefixCount = 1
279+
-- Fail: prefix count stays 0 = Prefix type not recognised, filter uses wrong slot
280+
it("counts a crafted prefix mod line", function()
281+
local modLines = {
282+
{ line = "35% increased Armour", crafted = true },
283+
}
284+
local result = mock_queryGen:CountCraftedAffixesFromModLines(modLines, syntheticPool, nil)
285+
assert.are.equal(1, result.prefix)
286+
assert.are.equal(0, result.suffix)
287+
end)
288+
289+
-- Pass: Non-crafted mod lines are ignored; counts stay zero
290+
-- Fail: Any non-zero count means regular mods trigger spurious slot filters
291+
it("ignores non-crafted mod lines", function()
292+
local modLines = {
293+
{ line = "+45 to maximum Energy Shield", crafted = false },
294+
}
295+
local result = mock_queryGen:CountCraftedAffixesFromModLines(modLines, syntheticPool, nil)
296+
assert.are.equal(0, result.prefix)
297+
assert.are.equal(0, result.suffix)
298+
end)
241299
end)
242300
end)

src/Classes/Item.lua

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@ local catalystTags = {
2424
{ "jewellery_elemental" ,"elemental_damage" },
2525
{ "critical" },
2626
}
27+
-- Maps game clipboard "Quality (X Modifiers)" label to catalyst index (same order as catalystList/catalystTags).
28+
local catalystQualityModifier = {
29+
["Attack Modifiers"] = 1,
30+
["Speed Modifiers"] = 2,
31+
["Life and Mana Modifiers"] = 3,
32+
["Caster Modifiers"] = 4,
33+
["Attribute Modifiers"] = 5,
34+
["Physical and Chaos Modifiers"]= 6,
35+
["Resistance Modifiers"] = 7,
36+
["Defense Modifiers"] = 8,
37+
["Elemental Modifiers"] = 9,
38+
["Critical Modifiers"] = 10,
39+
}
2740

2841
local function getCatalystScalar(catalystId, tags, quality)
2942
if not catalystId or type(catalystId) ~= "number" or not catalystTags[catalystId] or not tags or type(tags) ~= "table" or #tags == 0 then
@@ -582,19 +595,8 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
582595
self.catalystQuality = specToNumber(specVal)
583596
elseif specName:match("^Quality %((.-)%)$") and not self.catalyst then
584597
-- Game clipboard format: "Quality (Life and Mana Modifiers): +20% (augmented)"
585-
local qualityModifierToCatalyst = {
586-
["Attack Modifiers"] = 1,
587-
["Speed Modifiers"] = 2,
588-
["Life and Mana Modifiers"] = 3,
589-
["Caster Modifiers"] = 4,
590-
["Attribute Modifiers"] = 5,
591-
["Physical and Chaos Modifiers"] = 6,
592-
["Resistance Modifiers"] = 7,
593-
["Defense Modifiers"] = 8,
594-
["Elemental Modifiers"] = 9,
595-
["Critical Modifiers"] = 10,
596-
}
597-
local catalystId = qualityModifierToCatalyst[specName:match("^Quality %((.-)%)$")]
598+
local qualityModType = specName:match("^Quality %((.-)%)$")
599+
local catalystId = catalystQualityModifier[qualityModType]
598600
if catalystId then
599601
self.catalyst = catalystId
600602
self.catalystQuality = tonumber(specVal:match("(%d+)"))

src/Classes/TradeQueryGenerator.lua

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ local tradeStatCategoryIndices = {
8282
["PassiveNode"] = 2,
8383
}
8484

85+
-- Strips numeric tokens and punctuation for text-based mod matching.
86+
local function normalizeModText(text)
87+
return text:gsub("[#()0-9%-%+%.]",""):gsub("%s+", " ")
88+
end
89+
8590
local influenceSuffixes = { "_shaper", "_elder", "_adjudicator", "_basilisk", "_crusader", "_eyrie"}
8691
local influenceDropdownNames = { "None" }
8792
local hasInfluenceModIds = { }
@@ -125,21 +130,21 @@ local function normTradeText(text)
125130
return (text:match("^%s*(.-)%s*$"))
126131
end
127132

128-
-- Forward declaration defined below, after the class method definitions.
133+
-- Forward declaration - defined below, after the class method definitions.
129134
local buildAndCachePseudoMapping
130135

131136
-- Hard-coded fallbacks for pseudo texts that don't normalize to the same form as any explicit/implicit text.
132137
-- Key = normalized pseudo text; value = list of explicit/implicit tradeModIds to map to that pseudo.
133138
local pseudoNormFallbacks = {
134-
-- Pseudo "+#% total Attack Speed" "attack speed"; explicit "#% increased Attack Speed" "increased attack speed"
139+
-- Pseudo "+#% total Attack Speed" -> "attack speed"; explicit "#% increased Attack Speed" -> "increased attack speed"
135140
["attack speed"] = { "explicit.stat_681332047", "implicit.stat_681332047" },
136-
-- Pseudo "+#% total Cast Speed" "cast speed"; explicit "#% increased Cast Speed" "increased cast speed"
141+
-- Pseudo "+#% total Cast Speed" -> "cast speed"; explicit "#% increased Cast Speed" -> "increased cast speed"
137142
["cast speed"] = { "explicit.stat_2891184298", "implicit.stat_2891184298" },
138-
-- Pseudo "+#% Global Critical Strike Chance" "global critical strike chance";
139-
-- explicit "#% increased Global Critical Strike Chance" "increased global critical strike chance"
143+
-- Pseudo "+#% Global Critical Strike Chance" -> "global critical strike chance";
144+
-- explicit "#% increased Global Critical Strike Chance" -> "increased global critical strike chance"
140145
["global critical strike chance"] = { "explicit.stat_587431675", "implicit.stat_587431675" },
141-
-- Pseudo "+#% total Critical Strike Chance for Spells" "critical strike chance for spells";
142-
-- explicit "#% increased Critical Strike Chance for Spells" "increased critical strike chance for spells"
146+
-- Pseudo "+#% total Critical Strike Chance for Spells" -> "critical strike chance for spells";
147+
-- explicit "#% increased Critical Strike Chance for Spells" -> "increased critical strike chance for spells"
143148
["critical strike chance for spells"] = { "explicit.stat_737908626", "implicit.stat_737908626" },
144149
}
145150

@@ -456,6 +461,9 @@ function TradeQueryGeneratorClass:InitMods()
456461
end
457462
else
458463
local tradeStats = fetchStats()
464+
if not tradeStats then
465+
return
466+
end
459467
tradeStats:gsub("\n", " ")
460468
local tradeQueryStatsParsed = dkjson.decode(tradeStats)
461469
buildAndCachePseudoMapping(tradeQueryStatsParsed)
@@ -476,6 +484,9 @@ function TradeQueryGeneratorClass:InitMods()
476484

477485
-- originates from: https://www.pathofexile.com/api/trade/data/stats
478486
local tradeStats = fetchStats()
487+
if not tradeStats then
488+
return
489+
end
479490
tradeStats:gsub("\n", " ")
480491
local tradeQueryStatsParsed = dkjson.decode(tradeStats)
481492

@@ -1122,7 +1133,7 @@ function TradeQueryGeneratorClass:CountCraftedAffixesFromModLines(modLines, item
11221133
for modId, mod in pairs(pool) do
11231134
if mod.types then -- only crafted mods use types instead of weightKey/weightVal
11241135
for _, line in ipairs(mod) do
1125-
local key = line:gsub("[#()0-9%-%+%.]", ""):gsub("%s+", " "):match("^%s*(.-)%s*$")
1136+
local key = normalizeModText(line):match("^%s*(.-)%s*$")
11261137
if key ~= "" and not lineToAffix[key] then
11271138
lineToAffix[key] = { affixType = mod.type, modId = modId }
11281139
end
@@ -1137,7 +1148,7 @@ function TradeQueryGeneratorClass:CountCraftedAffixesFromModLines(modLines, item
11371148
local prefixCount, suffixCount = 0, 0
11381149
for _, modLine in ipairs(modLines) do
11391150
if modLine.crafted then
1140-
local key = modLine.line:gsub("[#()0-9%-%+%.]", ""):gsub("%s+", " "):match("^%s*(.-)%s*$")
1151+
local key = normalizeModText(modLine.line):match("^%s*(.-)%s*$")
11411152
local match = lineToAffix[key]
11421153
if match and not seenModIds[match.modId] then
11431154
seenModIds[match.modId] = true
@@ -1311,32 +1322,35 @@ function TradeQueryGeneratorClass:FinishQuery()
13111322
-- maps to the (Local) trade stat rather than the global ring/jewel version.
13121323
local function buildTextLookup(modTypeData, preferLocal)
13131324
local lookup = {}
1314-
local localOverrides = {}
1325+
-- Only allocate the local-override table when the caller needs it.
1326+
local localOverrides = preferLocal and {} or nil
13151327
for _, entry in pairs(modTypeData) do
1316-
local key = entry.tradeMod.text:gsub("[#()0-9%-%+%.]",""):gsub("%s+", " ")
1328+
local key = normalizeModText(entry.tradeMod.text)
13171329
if key ~= "" and not lookup[key] then
13181330
lookup[key] = entry.tradeMod.id
13191331
end
13201332
-- overrideModLine is set for "(Local)" trade stats; its value is the item
13211333
-- display text without " (Local)", which is what modLine.line will contain.
1322-
local overrideLine = entry.specialCaseData and entry.specialCaseData.overrideModLine
1323-
if overrideLine then
1324-
local overrideKey = overrideLine:gsub("[#()0-9%-%+%.]",""):gsub("%s+", " ")
1325-
if overrideKey ~= "" then
1326-
localOverrides[overrideKey] = entry.tradeMod.id
1334+
if localOverrides then
1335+
local overrideLine = entry.specialCaseData and entry.specialCaseData.overrideModLine
1336+
if overrideLine then
1337+
local overrideKey = normalizeModText(overrideLine)
1338+
if overrideKey ~= "" then
1339+
localOverrides[overrideKey] = entry.tradeMod.id
1340+
end
13271341
end
13281342
end
13291343
local singular = entry.specialCaseData and entry.specialCaseData.overrideModLineSingular
13301344
if singular then
1331-
local singKey = singular:gsub("[#()0-9%-%+%.]",""):gsub("%s+", " ")
1345+
local singKey = normalizeModText(singular)
13321346
if singKey ~= "" and not lookup[singKey] then
13331347
lookup[singKey] = entry.tradeMod.id
13341348
end
13351349
end
13361350
end
13371351
-- Merge local overrides last so they win over any global entry with the same text.
1338-
-- Only do this for item types that actually carry local mods (weapons and armour).
1339-
if preferLocal then
1352+
-- Only done for item types that actually carry local mods (weapons and armour).
1353+
if localOverrides then
13401354
for k, v in pairs(localOverrides) do
13411355
lookup[k] = v
13421356
end
@@ -1347,11 +1361,10 @@ function TradeQueryGeneratorClass:FinishQuery()
13471361
local explicitTextToId = buildTextLookup(self.modData.Explicit, preferLocal)
13481362
local implicitTextToId = buildTextLookup(self.modData.Implicit)
13491363
if options.includeEldritch then
1350-
for k, v in pairs(buildTextLookup(self.modData.Eater)) do
1351-
explicitTextToId[k] = explicitTextToId[k] or v
1352-
end
1353-
for k, v in pairs(buildTextLookup(self.modData.Exarch)) do
1354-
explicitTextToId[k] = explicitTextToId[k] or v
1364+
for _, eldritchModType in ipairs({ "Eater", "Exarch" }) do
1365+
for k, v in pairs(buildTextLookup(self.modData[eldritchModType])) do
1366+
explicitTextToId[k] = explicitTextToId[k] or v
1367+
end
13551368
end
13561369
end
13571370

@@ -1387,7 +1400,7 @@ function TradeQueryGeneratorClass:FinishQuery()
13871400
local function addModLines(modLines, primaryLookup, fallbackLookup)
13881401
for _, modLine in ipairs(modLines) do
13891402
if not modLine.crafted then
1390-
local matchStr = modLine.line:gsub("[#()0-9%-%+%.]",""):gsub("%s+", " ")
1403+
local matchStr = normalizeModText(modLine.line)
13911404
local tradeModId = primaryLookup[matchStr]
13921405
local usedFallback = false
13931406
if not tradeModId and fallbackLookup then

0 commit comments

Comments
 (0)