Skip to content

Commit d07ef7c

Browse files
vaisestLocalIdentity
andauthored
Port various PoB2 buy similar and comparison tab PRs (#9857)
* Port PoB2 buy similar mod matching and UI * Fix edit boxes * Re-export crucible data * Fix timeless jewel and rebuildurl on league and realm selectors * Fix league/realm selectors not rebuilding url and make them persistent * Add findTradeIdOption tests * Remove stat json * Port PoB2 socket position views * Fix window title changing in compare tab * Fix cluster jewel enchant handling * Run CI on updated image * Unfix mod export hashes (unused here and inflate the PR a lot) * Comment out cluster and crucible hashes * Regenerate QueryMods.lua * Apply @LocalIdentity's rebuildUrl() fix instead * Also add the test * Fix spell checker issue * Fix some trade issues + add test League selections now use the realm Trade stat lookup is now cached as it wasn't using it before Use isValueInArray function in place of extra code Add some tests and fix others --------- Co-authored-by: LocalIdentity <localidentity2@gmail.com>
1 parent 205da71 commit d07ef7c

23 files changed

Lines changed: 149639 additions & 69070 deletions
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
describe("Buy similar mod stat matching", function()
2+
local bs = LoadModule("Classes/CompareBuySimilar")
3+
local dkjson = require "dkjson"
4+
5+
describe("addModEntries mod matching", function()
6+
it("matches impossible escape mods as options", function()
7+
local fromNothing = new("Item", [[
8+
Impossible Escape
9+
Viridian Jewel
10+
LevelReq: 0
11+
Radius: Small
12+
Limited to: 1
13+
Implicits: 0
14+
Passive skills in radius of Elemental Overload can be Allocated without being connected to your tree
15+
Corrupted]])
16+
17+
local modSources = {
18+
{ list = fromNothing.explicitModLines, type = "explicit" }
19+
}
20+
local modEntries = bs.addModEntries(fromNothing, modSources)
21+
assert.equal(1, #modEntries)
22+
assert.same(
23+
{
24+
formattedLines = { colorCodes.MAGIC .. "Passive skills in radius of Elemental Overload can be Allocated without being connected to your tree" },
25+
type = "explicit",
26+
isOption = true,
27+
invert = false,
28+
tradeIds = { "explicit.stat_2422708892" },
29+
value = 22088
30+
},
31+
modEntries[1])
32+
end)
33+
34+
it("matches thread of hope radius as an option", function()
35+
local thread = new("Item", [[
36+
Rarity: UNIQUE
37+
Thread of Hope
38+
Crimson Jewel
39+
Radius: Variable
40+
Implicits: 0
41+
Only affects Passives in Massive Ring
42+
-15% to all Elemental Resistances
43+
Passive Skills in Radius can be Allocated without being connected to your tree
44+
Passage]])
45+
local modSources = {
46+
{ list = thread.explicitModLines, type = "explicit" }
47+
}
48+
local modEntries = bs.addModEntries(thread, modSources)
49+
assert.equal(4, #modEntries)
50+
assert.equal("explicit.stat_3642528642", modEntries[1].tradeIds[1])
51+
assert.equal(5, modEntries[1].value)
52+
end)
53+
54+
it("combines mods that are the same stat", function()
55+
local lifeDiamond = new("Item", [[
56+
Test Subject
57+
Diamond
58+
Implicits: 0
59+
+100 to Maximum Life
60+
+50 to Maximum Life
61+
+50% to Fire Resistance]])
62+
63+
local entries = bs.addModEntries(lifeDiamond, { { list = lifeDiamond.explicitModLines, type = "explicit" } })
64+
assert.equal(2, #entries)
65+
assert.equal(2, #entries[1].formattedLines)
66+
assert.equal("+100 to Maximum Life", StripEscapes(entries[1].formattedLines[1]))
67+
assert.equal("+50 to Maximum Life", StripEscapes(entries[1].formattedLines[2]))
68+
assert.equal(150, entries[1].value)
69+
70+
local lifelessDiamond = new("Item", [[
71+
Test Subject
72+
Diamond
73+
Implicits: 0
74+
-100 to Maximum Life
75+
+50 to Maximum Life
76+
+50% to Fire Resistance]])
77+
local entries = bs.addModEntries(lifelessDiamond,
78+
{ { list = lifelessDiamond.explicitModLines, type = "explicit" } })
79+
assert.equal(2, #entries)
80+
assert.equal(2, #entries[1].formattedLines)
81+
assert.equal(-50, entries[1].value)
82+
end)
83+
84+
it("is not case-sensitive", function ()
85+
local funnyItem = new("Item", [[
86+
Test Subject
87+
Diamond
88+
Implicits: 1
89+
+50 tO MaxIMum lifE]])
90+
91+
local entries = bs.addModEntries(funnyItem, {{list = funnyItem.implicitModLines, type = "implicit"}})
92+
assert.equal(1, #entries)
93+
end)
94+
95+
it("does not combine implicit and explicit mods", function()
96+
local lifelessDiamond = new("Item", [[
97+
Test Subject
98+
Diamond
99+
Implicits: 1
100+
-100 to Maximum Life
101+
+50 to Maximum Life]])
102+
local entries = bs.addModEntries(lifelessDiamond,
103+
{ { list = lifelessDiamond.implicitModLines, type = "implicit" }, { list = lifelessDiamond.explicitModLines, type = "explicit" } })
104+
assert.equal(2, #entries)
105+
assert.equal(-100, entries[1].value)
106+
assert.equal(50, entries[2].value)
107+
end)
108+
end)
109+
describe("popup URL controls", function()
110+
local originalCopy
111+
local originalOpenURL
112+
local originalFetchLeagues
113+
local copiedUrl
114+
local leaguesByRealm = {
115+
pc = { "PC League", "PC Event", "Standard" },
116+
sony = { "PS4 League", "Standard" },
117+
xbox = { "Xbox League", "Standard" },
118+
}
119+
120+
before_each(function()
121+
newBuild()
122+
originalCopy = _G.Copy
123+
originalOpenURL = _G.OpenURL
124+
copiedUrl = nil
125+
local requests = build.itemsTab.tradeQuery.tradeQueryRequests
126+
originalFetchLeagues = requests.FetchLeagues
127+
requests.FetchLeagues = function(_, realm, callback)
128+
callback(leaguesByRealm[realm])
129+
end
130+
end)
131+
132+
after_each(function()
133+
build.itemsTab.tradeQuery.tradeQueryRequests.FetchLeagues = originalFetchLeagues
134+
_G.Copy = originalCopy
135+
_G.OpenURL = originalOpenURL
136+
bs.lastRealmIdx = nil
137+
bs.lastLeagueByRealm = nil
138+
bs.lastListedIndex = nil
139+
main:ClosePopup()
140+
end)
141+
142+
local function openPopup(item, slotName)
143+
item = item or new("Item", "Rarity: Rare\nTest Ring\nRuby Ring\nImplicits: 0\n+50 to maximum Life")
144+
bs.openPopup(item, slotName or "Ring", build)
145+
local controls = main.popups[1].controls
146+
_G.Copy = function(url) copiedUrl = url end
147+
_G.OpenURL = function() end
148+
return controls
149+
end
150+
151+
local function getQuery(controls)
152+
controls.search.onClick()
153+
local queryJson = urlDecode(assert(copiedUrl:match("[?&]q=(.+)$")))
154+
return dkjson.decode(queryJson)
155+
end
156+
157+
it("rebuilds the URL when league and listed status change", function()
158+
local controls = openPopup()
159+
getQuery(controls)
160+
local initialUrl = copiedUrl
161+
162+
controls.leagueDrop:SetSel(isValueInArray(controls.leagueDrop.list, "Standard"))
163+
getQuery(controls)
164+
assert.not_equal(initialUrl, copiedUrl)
165+
assert.is_truthy(copiedUrl:find("/Standard?", 1, true))
166+
local standardUrl = copiedUrl
167+
168+
controls.listedDrop:SetSel(4)
169+
local query = getQuery(controls)
170+
assert.not_equal(standardUrl, copiedUrl)
171+
assert.equal("any", query.query.status.option)
172+
end)
173+
174+
it("persists league choices by name for each realm", function()
175+
local controls = openPopup()
176+
assert.equal("PC Event", controls.leagueDrop:GetSelValue())
177+
controls.leagueDrop:SetSel(2)
178+
controls.leagueDrop:SetSel(1)
179+
controls.realmDrop:SetSel(3)
180+
assert.equal("Xbox League", controls.leagueDrop:GetSelValue())
181+
controls.leagueDrop:SetSel(2)
182+
controls.realmDrop:SetSel(1)
183+
assert.equal("PC Event", controls.leagueDrop:GetSelValue())
184+
controls.leagueDrop:SetSel(2)
185+
controls.realmDrop:SetSel(3)
186+
assert.equal("Standard", controls.leagueDrop:GetSelValue())
187+
controls.listedDrop:SetSel(4)
188+
main:ClosePopup()
189+
190+
controls = openPopup()
191+
assert.equal(3, bs.lastRealmIdx)
192+
assert.equal("Standard", controls.leagueDrop:GetSelValue())
193+
assert.equal("Any", controls.listedDrop:GetSelValue())
194+
end)
195+
196+
it("encodes option values in the generated query", function()
197+
local item = new("Item", [[
198+
Rarity: UNIQUE
199+
Impossible Escape
200+
Viridian Jewel
201+
LevelReq: 0
202+
Radius: Small
203+
Limited to: 1
204+
Implicits: 0
205+
Passive skills in radius of Elemental Overload can be Allocated without being connected to your tree
206+
Corrupted]])
207+
local controls = openPopup(item, "Jewel")
208+
controls.mod1Check.state = true
209+
controls.mod1Check.changeFunc()
210+
211+
local filter = getQuery(controls).query.stats[1].filters[1]
212+
assert.equal("explicit.stat_2422708892", filter.id)
213+
assert.equal(22088, filter.value.option)
214+
end)
215+
216+
it("inverts reduced stat bounds in the generated query", function()
217+
local item = new("Item", [[
218+
Rarity: Rare
219+
Test Ring
220+
Ruby Ring
221+
Implicits: 0
222+
67% reduced maximum Life]])
223+
local controls = openPopup(item)
224+
controls.mod1Check.state = true
225+
controls.mod1Check.changeFunc()
226+
227+
local value = getQuery(controls).query.stats[1].filters[1].value
228+
assert.is_nil(value.min)
229+
assert.equal(-67, value.max)
230+
end)
231+
232+
it("uses a count group for ambiguous trade stats", function()
233+
local item = new("Item", [[
234+
Rarity: Rare
235+
Test Ring
236+
Ruby Ring
237+
Implicits: 0
238+
Adds 5 to 15 Fire Damage]])
239+
local modEntries = bs.addModEntries(item, { { list = item.explicitModLines, type = "explicit" } })
240+
local ambiguousIndex
241+
for index, entry in ipairs(modEntries) do
242+
if #entry.tradeIds > 1 then
243+
ambiguousIndex = index
244+
break
245+
end
246+
end
247+
assert.is_truthy(ambiguousIndex)
248+
249+
local controls = openPopup(item)
250+
local check = controls["mod" .. ambiguousIndex .. "Check"]
251+
check.state = true
252+
check.changeFunc()
253+
254+
local countFilter = getQuery(controls).query.stats[2]
255+
assert.equal("count", countFilter.type)
256+
assert.equal(1, countFilter.value.min)
257+
assert.equal(#modEntries[ambiguousIndex].tradeIds, #countFilter.filters)
258+
end)
259+
end)
260+
end)
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
describe("TradeHelpers trade hash matching", function()
2+
local tradeHelpers = LoadModule("Classes/TradeHelpers")
3+
4+
describe("modLineValue", function()
5+
it("returns the single number on a line", function()
6+
assert.equal(50, tradeHelpers.modLineValue("+50 to maximum Life"))
7+
end)
8+
9+
it("returns the midpoint of a '# to #' range", function()
10+
assert.equal(15, tradeHelpers.modLineValue("Adds 10 to 20 Fire Damage"))
11+
assert.equal(12.5, tradeHelpers.modLineValue("Adds 10 to 15 Fire Damage"))
12+
end)
13+
14+
it("handles negative numbers", function()
15+
assert.equal(-10, tradeHelpers.modLineValue("-10% to Fire Resistance"))
16+
end)
17+
18+
it("returns nil when onlyFromTo is set and there is no range", function()
19+
assert.is_nil(tradeHelpers.modLineValue("+50 to maximum Life", true))
20+
end)
21+
end)
22+
23+
describe("findTradeIdOption", function()
24+
it("matches a '#'-valued option and returns its value", function()
25+
local tradeId, value = tradeHelpers.findTradeIdOption("Grants Level 20 Summon Bestial Snake Skill",
26+
"explicit")
27+
assert.equal("explicit.stat_2878779644", tradeId)
28+
assert.equal(3, value)
29+
end)
30+
31+
it("matches an exact-text option and returns no value", function()
32+
local tradeId, value = tradeHelpers.findTradeIdOption("Allocates Tranquility", "enchant")
33+
assert.equal("enchant.stat_2954116742", tradeId)
34+
assert.equal(16246, value)
35+
end)
36+
37+
it("matches a timeless jewel", function()
38+
local tradeId, value = tradeHelpers.findTradeIdOption(
39+
"Bathed in the blood of 666 sacrificed in the name of Doryani", "explicit")
40+
assert.equal("explicit.pseudo_timeless_jewel_doryani", tradeId)
41+
assert.equal(666, value)
42+
end)
43+
44+
it("returns nil for an unmatchable line", function()
45+
assert.is_nil(tradeHelpers.findTradeIdOption("+100 to IQ", "explicit"))
46+
end)
47+
end)
48+
describe("findTradeHash", function()
49+
it("matches a simple mod", function()
50+
local ids, value = tradeHelpers.findTradeHash("+50 to maximum Life")
51+
assert.equal(50, value)
52+
assert.is_truthy(isValueInArray(ids, HashStats({ "base_maximum_life" })))
53+
end)
54+
55+
it("matches a percentage mod", function()
56+
local ids, value = tradeHelpers.findTradeHash("25% reduced maximum Energy Shield")
57+
assert.equal(25, value)
58+
assert.is_truthy(isValueInArray(ids, HashStats({ "maximum_energy_shield_+%" })))
59+
end)
60+
61+
it("matches a # to # mod", function()
62+
local ids, value = tradeHelpers.findTradeHash("Adds 5 to 15 Fire Damage")
63+
assert.equal(10, value)
64+
assert.is_truthy(isValueInArray(ids,
65+
HashStats({ "local_minimum_added_fire_damage", "local_maximum_added_fire_damage" })))
66+
end)
67+
68+
it("is case-insensitive", function()
69+
local ids = tradeHelpers.findTradeHash(
70+
"1 aDDED PASSiVe sKilL IS ONE wITh The ShiELd")
71+
assert.is_truthy(isValueInArray(ids, HashStats({ "local_affliction_notable_one_with_the_shield" })))
72+
end)
73+
74+
it("returns no results for an unmatchable line", function()
75+
local ids = tradeHelpers.findTradeHash("+100 to IQ")
76+
assert.equal(0, #ids)
77+
end)
78+
79+
it("detects inverted mods correctly", function()
80+
-- note that this stat is a handwrap mod and doesn't actually exist on the trade site
81+
local ids, value, shouldNegate = tradeHelpers.findTradeHash(
82+
"Debuffs on you expire 100% slower while affected by Haste")
83+
assert.equal(100, value)
84+
assert.is_true(shouldNegate)
85+
assert.equal(1, #ids)
86+
87+
local ids, value, shouldNegate = tradeHelpers.findTradeHash("67% reduced maximum life")
88+
assert.equal(67, value)
89+
assert.is_true(shouldNegate)
90+
assert.equal(1, #ids)
91+
end)
92+
it("detects mods with lua pattern characters correctly", function()
93+
local ids, value = tradeHelpers.findTradeHash(
94+
"trigger Socketed Spells when you focus, with a 0.25 second cooldown")
95+
assert.is_truthy(isValueInArray(ids, HashStats({ "trigger_socketed_spells_when_you_focus_%" })))
96+
assert.equal(100, value)
97+
98+
local ids, value, shouldNegate = tradeHelpers.findTradeHash(
99+
"10% reduced effect of Non-Curse Auras from your Skills on your Minions")
100+
assert.is_truthy(isValueInArray(ids, HashStats({ "minions_have_non_curse_aura_effect_+%_from_parent_skills" })))
101+
assert.equal(10, value)
102+
assert.is_true(shouldNegate)
103+
end)
104+
it("detects passage modline correctly", function()
105+
local ids = tradeHelpers.findTradeHash(
106+
"Passive Skills in Radius can be Allocated without being connected to your tree")
107+
assert.is_truthy(isValueInArray(ids,
108+
HashStats({ "local_unique_jewel_nearby_disconnected_passives_can_be_allocated",
109+
"unique_thread_of_hope_base_resist_all_elements_%" })))
110+
end)
111+
end)
112+
end)

0 commit comments

Comments
 (0)