Skip to content

Commit 74a0f48

Browse files
KenishiLocalIdentity
andauthored
Add support for new socketables & Handle slot specific runes (#1157)
* Add support for Runes on Wand / Staff GGG added more runes for wands and staves in the patch a couple weeks ago I don't particularly like how this is done as it requires more individual base checks each time a soul core or rune can go into a different base This becomes even more of a problem when trying to support the new talismans that can be socketed into gloves, boots, helmets, body armours and sceptres * Implement new runes, talismans, and soul cores. * Edit comment * Fix test to use build object --------- Co-authored-by: LocalIdentity <localidentity2@gmail.com>
1 parent b5ff326 commit 74a0f48

16 files changed

Lines changed: 2155 additions & 255 deletions

spec/System/TestItemParse_spec.lua

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,4 +248,24 @@ describe("TestItemParse", function()
248248
assert.are.equals(71, item.requirements.intMod)
249249

250250
end)
251+
252+
it("multi-line rune mod", function()
253+
-- Thruldana is Bow-only as well
254+
local item = new("Item", [[
255+
Test Item
256+
Crude Bow
257+
Quality: 20
258+
Sockets: S S
259+
Rune: Talisman of Thruldana
260+
Rune: Talisman of Thruldana
261+
Implicits: 2
262+
{enchant}{rune}50% reduced Poison Duration
263+
{enchant}{rune}Targets can be affected by +2 of your Poisons at the same time
264+
]])
265+
item:BuildAndParseRaw()
266+
267+
assert.are.equals(2, #item.sockets)
268+
assert.are.equals(2, #item.runeModLines)
269+
270+
end)
251271
end)
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
describe("TestSocketables", function()
2+
before_each(function()
3+
newBuild()
4+
end)
5+
6+
it("ModRunes matches Data/Soulcores", function()
7+
local modRunes = LoadModule("../src/Data/ModRunes")
8+
local soulcores = {}
9+
LoadModule("../src/Data/Bases/soulcore", soulcores)
10+
local soulCoreCount = 0
11+
for name, _ in pairs(soulcores) do
12+
assert.is_not.equals(modRunes[name], nil)
13+
soulCoreCount = soulCoreCount + 1
14+
end
15+
16+
local modRunesCount = 0
17+
for name, _ in pairs(modRunes) do
18+
assert.is_not.equals(soulcores[name], nil)
19+
modRunesCount = modRunesCount + 1
20+
end
21+
-- Final check that Bases/soulcore has same number of entries as ModRunes
22+
assert.are.equals(modRunesCount, soulCoreCount)
23+
end)
24+
25+
-- Item Tab display Tests
26+
-- Also checks slot type runes
27+
28+
local extractNamesFromModRunes = function(slotType)
29+
local modRunes = LoadModule("../src/Data/ModRunes")
30+
local names = { }
31+
for name, rune in pairs(modRunes) do
32+
for runeSlotType, mods in pairs(rune) do
33+
if runeSlotType == slotType then
34+
-- Need to add an entry of the name for each mod line for tests
35+
for _, _ in ipairs(mods) do
36+
table.insert(names, name)
37+
end
38+
end
39+
end
40+
end
41+
return names
42+
end
43+
44+
local slotTypeTest = function(slotType, itemBase)
45+
-- ConPrintf("Testing: %s", slotType)
46+
local itemRaw = "Test\n" .. itemBase .. "\nSockets: S"
47+
48+
local modRunes = extractNamesFromModRunes(slotType)
49+
50+
-- Create an ItemTab and add a socketable item to it
51+
local item = new("Item", itemRaw)
52+
53+
build.itemsTab:AddItem(item)
54+
build.itemsTab:SetDisplayItem(item)
55+
runCallback("OnFrame")
56+
57+
-- Extract the proper slot type runes from the list
58+
local itemTabRunes = { }
59+
for _, rune in ipairs(build.itemsTab.controls["displayItemRune1"].list) do
60+
if rune.slot == slotType then
61+
table.insert(itemTabRunes, rune.name)
62+
end
63+
end
64+
-- To keep the test fast, only check that the lengths match
65+
-- This should also catch issues with multi-mod line runes since the rune name will appear
66+
-- for the number of mod lines that the rune has.
67+
assert.are.equals(#itemTabRunes, #modRunes)
68+
end
69+
70+
-- Note: Except for weapon/armour/caster,
71+
-- "slotType" references the dat file ItemClasses.Id value as this is what dat file SoulCoresPerClass.ItemClass refs
72+
-- Not all item classes have runes yet
73+
it("'Weapon' runes appear in Items tab", slotTypeTest("weapon", "Massive Greathammer"))
74+
75+
it("'Armour' runes appear in Items tab", slotTypeTest("armour", "Slayer Armour"))
76+
77+
it("'Caster' runes appear in Items tab", slotTypeTest("caster", "Bone Wand"))
78+
79+
it("'Body Armour' runes appear in Items tab", slotTypeTest("body armour", "Slayer Armour"))
80+
81+
it("'Helmets' runes appear in Items tab", slotTypeTest("helmet", "Kamasan Tiara"))
82+
83+
it("'Gloves' runes appear in Items tab", slotTypeTest("gloves", "Vaal Gloves"))
84+
85+
it("'Boots' runes appear in Items tab", slotTypeTest("boots", "Vaal Greaves"))
86+
87+
it("'Shield' runes appear in Items tab", slotTypeTest("shield", "Vaal Tower Shield"))
88+
89+
it("'Focus' runes appear in Items tab", slotTypeTest("focus", "Hallowed Focus"))
90+
91+
-- Weapons
92+
it("'Bow' runes appear in Items tab", slotTypeTest("bow", "Gemini Bow"))
93+
94+
it("'Crossbow' runes appear in Items tab", slotTypeTest("crossbow", "Siege Crossbow"))
95+
96+
it("'Wand' runes appear in Items tab", slotTypeTest("wand", "Bone Wand"))
97+
98+
it("'Sceptre' runes appear in Items tab", slotTypeTest("sceptre", "Omen Sceptre"))
99+
100+
it("'(Caster) Staff' runes appear in Items tab", slotTypeTest("staff", "Voltaic Staff"))
101+
102+
it("'(Quarterstaff) War Staff' runes appear in Items tab", slotTypeTest("warstaff", "Striking Quarterstaff"))
103+
104+
it("'Spear' runes appear in Items tab", slotTypeTest("spear", "Flying Spear"))
105+
106+
it("'One Hand Mace' runes appear in Items tab", slotTypeTest("one hand mace", "Marauding Mace"))
107+
108+
it("'Two Hand Mace' runes appear in Items tab", slotTypeTest("two hand mace", "Massive Greathammer"))
109+
110+
-- Not Yet Added
111+
-- it("'One Hand Sword' runes appear in Items tab", slotTypeTest("one hand sword", ""))
112+
113+
-- it("'Two Hand Sword' runes appear in Items tab", slotTypeTest("two hand sword", ""))
114+
115+
-- it("'One Hand Axe' runes appear in Items tab", slotTypeTest("one hand axe", ""))
116+
117+
-- it("'Two Hand Axe' runes appear in Items tab", slotTypeTest("two hand axe", ""))
118+
119+
-- it("'Flail' runes appear in Items tab", slotTypeTest("flail", ""))
120+
121+
-- Future note: Once traps are added, verify that GGG stayed with "traptool"
122+
-- it("'Trap' runes appear in Items tab", slotTypeTest("traptool", ""))
123+
124+
-- it("'Claw' runes appear in Items tab", slotTypeTest("claw", ""))
125+
126+
-- it("'Dagger' runes appear in Items tab", slotTypeTest("dagger", ""))
127+
128+
end)

src/Classes/Item.lua

Lines changed: 63 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -791,26 +791,35 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
791791
end
792792
-- this will need more advanced logic for jewel sockets in items to work properly but could just be removed as items like this was only introduced during development.
793793
if self.base then
794-
if self.base.weapon or self.base.armour then
794+
if self.base.weapon or self.base.armour or self.base.tags.wand or self.base.tags.staff or self.base.tags.sceptre then
795795
local shouldFixRunesOnItem = #self.runes == 0
796796

797797
-- Form a key value table with the following format
798798
-- { [strippedModLine] = { { runeName1, runeValue1 }, etc, }, etc}
799799
-- This will be used to more easily grab the relevant runes that combinations will need to be of.
800800
-- This could be refactored to only needs to be called once.
801801
local statGroupedRunes = { }
802-
local type = self.base.weapon and "weapon" or "armour" -- minor optimisation
802+
local broadItemType = self.base.weapon and "weapon" or (self.base.tags.wand or self.base.tags.staff) and "caster" or "armour" -- minor optimisation
803+
local specificItemType = self.base.type:lower()
803804
for runeName, runeMods in pairs(data.itemMods.Runes) do
804-
-- gets the first value in the mod and its stripped line.
805-
local runeValue
806-
local runeStrippedModeLine = runeMods[type][1]:gsub("(%d%.?%d*)", function(val)
807-
runeValue = val
808-
return "#"
809-
end)
810-
if statGroupedRunes[runeStrippedModeLine] == nil then
811-
statGroupedRunes[runeStrippedModeLine] = { }
805+
local addModToGroupedRunes = function (modLine)
806+
local runeValue
807+
local runeStrippedModLine = modLine:gsub("(%d%.?%d*)", function(val)
808+
runeValue = val
809+
return "#"
810+
end)
811+
if statGroupedRunes[runeStrippedModLine] == nil then
812+
statGroupedRunes[runeStrippedModLine] = { }
813+
end
814+
t_insert(statGroupedRunes[runeStrippedModLine], { runeName, runeValue });
815+
end
816+
for slotType, slotMod in pairs(runeMods) do
817+
if slotType == broadItemType or slotType == specificItemType then
818+
for _, mod in ipairs(slotMod) do
819+
addModToGroupedRunes(mod)
820+
end
821+
end
812822
end
813-
t_insert(statGroupedRunes[runeStrippedModeLine], { runeName, runeValue });
814823
end
815824

816825
-- Sort table to ensure first entries are always largest.
@@ -1192,7 +1201,7 @@ function ItemClass:BuildRaw()
11921201
if self.quality then
11931202
t_insert(rawLines, "Quality: " .. self.quality)
11941203
end
1195-
if self.itemSocketCount and self.itemSocketCount > 0 and (self.base.weapon or self.base.armour) then
1204+
if self.itemSocketCount and self.itemSocketCount > 0 then
11961205
local socketString = ""
11971206
for _ = 1, self.itemSocketCount do
11981207
socketString = socketString .. "S "
@@ -1248,31 +1257,53 @@ end
12481257
-- Rebuild rune modifiers using the item's runes
12491258
function ItemClass:UpdateRunes()
12501259
wipeTable(self.runeModLines)
1260+
local getModRunesForTypes = function(runeName, baseType, specificType)
1261+
local rune = data.itemMods.Runes[runeName]
1262+
local gatheredRuneMods = { }
1263+
if rune then
1264+
if rune[baseType] then
1265+
-- for _, mod in pairs(rune[baseType]) do
1266+
t_insert(gatheredRuneMods, rune[baseType])
1267+
-- end
1268+
end
1269+
if rune[specificType] then
1270+
-- for _, mod in pairs(rune[specificType]) do
1271+
t_insert(gatheredRuneMods, rune[specificType])
1272+
-- end
1273+
end
1274+
end
1275+
return gatheredRuneMods
1276+
end
1277+
12511278
local statOrder = {}
12521279
for i = 1, self.itemSocketCount do
12531280
local name = self.runes[i]
12541281
if name and name ~= "None" then
1255-
local mod = self.base.weapon and data.itemMods.Runes[name].weapon or self.base.armour and data.itemMods.Runes[name].armour or { }
1256-
for i, line in ipairs(mod) do
1257-
local order = mod.statOrder[i]
1258-
if statOrder[order] then
1259-
-- Combine stats
1260-
local start = 1
1261-
statOrder[order].line = statOrder[order].line:gsub("(%d%.?%d*)", function(num)
1262-
local s, e, other = line:find("(%d%.?%d*)", start)
1263-
start = e + 1
1264-
return tonumber(num) + tonumber(other)
1265-
end)
1266-
else
1267-
local modLine = { line = line, order = order, rune = true, enchant = true }
1268-
for l = 1, #self.runeModLines + 1 do
1269-
if not self.runeModLines[l] or self.runeModLines[l].order > order then
1270-
t_insert(self.runeModLines, l, modLine)
1271-
break
1282+
local baseType = self.base.weapon and "weapon" or self.base.armour and "armour" or (self.base.tags.wand or self.base.tags.staff) and "caster"
1283+
local specificType = self.base.type:lower()
1284+
local gatheredMods = getModRunesForTypes(name, baseType, specificType)
1285+
for _, mod in ipairs(gatheredMods) do
1286+
for i, modLine in ipairs(mod) do
1287+
local order = mod.statOrder[i]
1288+
if statOrder[order] then
1289+
-- Combine stats
1290+
local start = 1
1291+
statOrder[order].line = statOrder[order].line:gsub("(%d%.?%d*)", function(num)
1292+
local s, e, other = mod[i]:find("(%d%.?%d*)", start)
1293+
start = e + 1
1294+
return tonumber(num) + tonumber(other)
1295+
end)
1296+
else
1297+
local modLine = { line = modLine, order = order, rune = true, enchant = true }
1298+
for l = 1, #self.runeModLines + 1 do
1299+
if not self.runeModLines[l] or self.runeModLines[l].order > order then
1300+
t_insert(self.runeModLines, l, modLine)
1301+
break
1302+
end
12721303
end
1273-
end
1274-
statOrder[order] = modLine
1275-
end
1304+
statOrder[order] = modLine
1305+
end
1306+
end
12761307
end
12771308
end
12781309
end

src/Classes/ItemsTab.lua

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -366,11 +366,11 @@ holding Shift will put it in the second.]])
366366

367367
-- Section: Sockets and Links
368368
self.controls.displayItemSectionSockets = new("Control", {"TOPLEFT",self.controls.displayItemSectionVariant,"BOTTOMLEFT"}, {0, 0, 0, function()
369-
return self.displayItem and (self.displayItem.base.weapon or self.displayItem.base.armour) and 28 or 0
369+
return self.displayItem and (self.displayItem.base.weapon or self.displayItem.base.armour or self.displayItem.base.tags.wand or self.displayItem.base.tags.staff or self.displayItem.base.tags.sceptre) and 28 or 0
370370
end})
371371
self.controls.displayItemSocketRune = new("LabelControl", {"TOPLEFT",self.controls.displayItemSectionSockets,"TOPLEFT"}, {0, 0, 36, 20}, "^x7F7F7FS")
372372
self.controls.displayItemSocketRune.shown = function()
373-
return self.displayItem.base.weapon or self.displayItem.base.armour
373+
return self.displayItem.base.weapon or self.displayItem.base.armour or self.displayItem.base.tags.wand or self.displayItem.base.tags.staff or self.displayItem.base.tags.sceptre
374374
end
375375
self.controls.displayItemSocketRuneEdit = new("EditControl", {"LEFT",self.controls.displayItemSocketRune,"RIGHT"}, {2, 0, 50, 20}, nil, nil, "%D", 1, function(buf)
376376
if tonumber(buf) > 6 then
@@ -501,7 +501,7 @@ holding Shift will put it in the second.]])
501501

502502
-- Section: Rune Selection
503503
self.controls.displayItemSectionRune = new("Control", {"TOPLEFT",self.controls.displayItemSectionClusterJewel,"BOTTOMLEFT"}, {0, 0, 0, function()
504-
if not self.displayItem or self.displayItem.itemSocketCount == 0 or not (self.displayItem.base.weapon or self.displayItem.base.armour) then
504+
if not self.displayItem or self.displayItem.itemSocketCount == 0 or not (self.displayItem.base.weapon or self.displayItem.base.armour or self.displayItem.base.tags.wand or self.displayItem.base.tags.staff or self.displayItem.base.tags.sceptre) then
505505
return 0
506506
end
507507
local h = 6
@@ -534,7 +534,7 @@ holding Shift will put it in the second.]])
534534
end
535535
end
536536
drop.shown = function()
537-
return self.displayItem and i <= self.displayItem.itemSocketCount and (self.displayItem.base.weapon or self.displayItem.base.armour)
537+
return self.displayItem and i <= self.displayItem.itemSocketCount and (self.displayItem.base.weapon or self.displayItem.base.armour or self.displayItem.base.tags.wand or self.displayItem.base.tags.staff or self.displayItem.base.tags.sceptre)
538538
end
539539

540540
self.controls["displayItemRune"..i] = drop
@@ -1560,28 +1560,42 @@ function ItemsTabClass:UpdateAffixControls()
15601560
self:UpdateCustomControls()
15611561
end
15621562

1563-
-- build rune mod list for armour and weapons
1564-
local runeArmourModLines = { { name = "None", label = "None", order = -1 } }
1565-
local runeWeaponModLines = { { name = "None", label = "None", order = -1 } }
1566-
for name, modLines in pairs(data.itemMods.Runes) do
1567-
t_insert(runeArmourModLines, { name = name, label = modLines.armour[1], order = modLines.armour.statOrder[1]})
1568-
t_insert(runeWeaponModLines, { name = name, label = modLines.weapon[1], order = modLines.weapon.statOrder[1]})
1563+
local runeModLines = { { name = "None", label = "None", order = -1, slot = "None", group = -1 } }
1564+
for name, runeMods in pairs(data.itemMods.Runes) do
1565+
-- Some runes have multiple mod lines; insert each as separate entry
1566+
for slotType, runeMod in pairs(runeMods) do
1567+
for i, mod in ipairs(runeMod) do
1568+
t_insert(runeModLines, { name = name, label = mod, order = runeMod.statOrder[1], slot = slotType, group = #runeMod })
1569+
end
1570+
end
15691571
end
1570-
table.sort(runeArmourModLines, function(a, b)
1571-
return a.order < b.order
1572-
end)
1573-
table.sort(runeWeaponModLines, function(a, b)
1574-
return a.order < b.order
1572+
table.sort(runeModLines, function(a, b)
1573+
if a.order == b.order then
1574+
return a.label < b.label
1575+
elseif a.group == b.group then
1576+
return a.order < b.order
1577+
else
1578+
return a.group < b.group
1579+
end
15751580
end)
15761581
-- Update rune selection controls
15771582
function ItemsTabClass:UpdateRuneControls()
15781583
local item = self.displayItem
1579-
for i = 1, item.itemSocketCount do
1580-
if item.base.armour then
1581-
self.controls["displayItemRune"..i].list = runeArmourModLines
1582-
elseif item.base.weapon then
1583-
self.controls["displayItemRune"..i].list = runeWeaponModLines
1584+
-- Build rune selection for item
1585+
local runes = { }
1586+
for _, rune in pairs(runeModLines) do
1587+
if rune.slot == "None" or -- Needed "None" for Items Tab
1588+
item.base.type:lower() == rune.slot or
1589+
item.base.type == rune.slot or
1590+
item.base.weapon and rune.slot == "weapon" or
1591+
item.base.armour and rune.slot == "armour" or
1592+
(item.base.tags.wand or item.base.tags.staff) and rune.slot == "caster" then
1593+
table.insert(runes, rune)
15841594
end
1595+
end
1596+
1597+
for i = 1, item.itemSocketCount do
1598+
self.controls["displayItemRune"..i].list = runes
15851599
if item.runes[i] then
15861600
for j, modLine in ipairs(self.controls["displayItemRune"..i].list) do
15871601
if item.runes[i] == modLine.name then
@@ -1902,7 +1916,7 @@ function ItemsTabClass:CraftItem()
19021916
else
19031917
item.quality = nil
19041918
end
1905-
if base.base.socketLimit and (base.base.weapon or base.base.armour) then -- must be a martial weapon/armour
1919+
if base.base.socketLimit and (base.base.weapon or base.base.armour or base.base.tags.wand or base.base.tags.staff or base.base.tags.sceptre) then -- must be a martial weapon/armour
19061920
if #item.sockets == 0 then
19071921
for i = 1, base.base.socketLimit do
19081922
t_insert(item.sockets, { group = 0 })

0 commit comments

Comments
 (0)