Skip to content

Commit a2ba265

Browse files
committed
feat: implement Way of the Stonefist ascendancy mechanics
Implements the three core mechanics of the Stonefist ascendancy node: - GloveBaseTypeTransform: overwrites equipped glove armour/evasion/ES values with the Fists of Stone base stats and injects the three per-level implicit mods (+2 Evasion, +1 ES, +1 Ward per player level) into modDB each calc pass. Item state is restored after each pass via env.stonefistRestore. - IgnoreAttributeRequirementsForGloves: scoped flag that skips attribute requirement checks only for glove-slot items, without zeroing global attribute requirements. - GloveExplicitModTransform stub: flag parsed and ready for the mod-equivalency lookup phase (Data/ModEquivalencies.lua export script included). Also adds the Fists of Stone base type to Data/Bases/gloves.lua (armour 44, evasion 40, ES 15, quality 20), ModParser patterns for all three ascendancy mod strings, and a full Busted test suite in spec/System/TestStonefist_spec.lua.
1 parent dcffcbe commit a2ba265

7 files changed

Lines changed: 268 additions & 4 deletions

File tree

spec/System/TestItemParse_spec.lua

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,4 +664,30 @@ describe("TestAdvancedItemParse #item", function()
664664
Note: ~b/o 2 chaos
665665
]])
666666
end)
667+
668+
it("Has +N to Evasion Rating per player level parses to BASE Evasion with Level multiplier", function()
669+
local item = new("Item", [[
670+
Rarity: Rare
671+
Striker's Grip
672+
Fists of Stone
673+
--------
674+
Implicits: 3
675+
Has +2 to Evasion Rating per player level (implicit)
676+
Has +1 to maximum Energy Shield per player level (implicit)
677+
Has +1 to maximum Runic Ward per player level (implicit)
678+
]])
679+
-- All three implicit mod lines should be parsed (no extra/unsupported flag)
680+
assert.are.equals(3, #item.implicitModLines)
681+
assert.is_nil(item.implicitModLines[1].extra)
682+
assert.is_nil(item.implicitModLines[2].extra)
683+
assert.is_nil(item.implicitModLines[3].extra)
684+
-- Check raw mod values
685+
assert.are.equals(2, item.baseModList[1].value)
686+
assert.are.equals(1, item.baseModList[2].value)
687+
assert.are.equals(1, item.baseModList[3].value)
688+
-- Check mod names map to correct stats
689+
assert.are.equals("Evasion", item.baseModList[1].name)
690+
assert.are.equals("EnergyShield", item.baseModList[2].name)
691+
assert.are.equals("Ward", item.baseModList[3].name)
692+
end)
667693
end)

spec/System/TestStonefist_spec.lua

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
describe("TestStonefist", function()
2+
before_each(function()
3+
newBuild()
4+
end)
5+
6+
teardown(function()
7+
-- newBuild() takes care of resetting everything in setup()
8+
end)
9+
10+
-- ModParser: flag parsing
11+
12+
it("GloveBaseTypeTransform flag is set from ascendancy mod string", function()
13+
build.configTab.input.customMods = "\z
14+
Gloves you equip have their base type transformed to fists of stone while equipped\n\z
15+
"
16+
build.configTab:BuildModList()
17+
runCallback("OnFrame")
18+
19+
assert.is_true(build.calcsTab.mainEnv.modDB:Flag(nil, "GloveBaseTypeTransform"))
20+
end)
21+
22+
it("IgnoreAttributeRequirementsForGloves flag is set from ascendancy mod string", function()
23+
build.configTab.input.customMods = "\z
24+
Ignore attribute requirements to equip gloves\n\z
25+
"
26+
build.configTab:BuildModList()
27+
runCallback("OnFrame")
28+
29+
assert.is_true(build.calcsTab.mainEnv.modDB:Flag(nil, "IgnoreAttributeRequirementsForGloves"))
30+
end)
31+
32+
it("GloveExplicitModTransform flag is set from ascendancy mod string", function()
33+
build.configTab.input.customMods = "\z
34+
their explicit modifiers are transformed into more powerful related modifiers\n\z
35+
"
36+
build.configTab:BuildModList()
37+
runCallback("OnFrame")
38+
39+
assert.is_true(build.calcsTab.mainEnv.modDB:Flag(nil, "GloveExplicitModTransform"))
40+
end)
41+
42+
-- CalcPerform: base type transform overwrites glove armour values
43+
44+
it("GloveBaseTypeTransform: equipping pure-evasion gloves gains Armour from Fists of Stone base", function()
45+
-- Suede Bracers: Evasion only, no Armour stat
46+
build.itemsTab:CreateDisplayItemFromRaw([[
47+
New Item
48+
Suede Bracers
49+
Evasion: 10
50+
]])
51+
build.itemsTab:AddDisplayItem()
52+
runCallback("OnFrame")
53+
54+
local baseArmour = build.calcsTab.mainOutput.Armour or 0
55+
56+
-- Apply transform flag
57+
build.configTab.input.customMods = "\z
58+
Gloves you equip have their base type transformed to fists of stone while equipped\n\z
59+
"
60+
build.configTab:BuildModList()
61+
runCallback("OnFrame")
62+
63+
local transformedArmour = build.calcsTab.mainOutput.Armour or 0
64+
-- Fists of Stone base armour is 44; should exceed the evasion-only baseline
65+
assert.is_true(transformedArmour > baseArmour,
66+
("expected transformed armour %d > base armour %d"):format(transformedArmour, baseArmour))
67+
assert.is_near(44, transformedArmour, 10)
68+
end)
69+
70+
it("GloveBaseTypeTransform: armour-only gloves take on Fists of Stone armour value (~44)", function()
71+
-- Stocky Mitts base armour = 15; Fists of Stone base armour = 44
72+
build.itemsTab:CreateDisplayItemFromRaw([[
73+
New Item
74+
Stocky Mitts
75+
]])
76+
build.itemsTab:AddDisplayItem()
77+
runCallback("OnFrame")
78+
79+
local baseArmour = build.calcsTab.mainOutput.Armour or 0
80+
81+
build.configTab.input.customMods = "\z
82+
Gloves you equip have their base type transformed to fists of stone while equipped\n\z
83+
"
84+
build.configTab:BuildModList()
85+
runCallback("OnFrame")
86+
87+
local transformedArmour = build.calcsTab.mainOutput.Armour or 0
88+
-- Armour should change from Stocky Mitts base (~15) to Fists of Stone base (~44)
89+
assert.are_not.equals(baseArmour, transformedArmour)
90+
assert.is_near(44, transformedArmour, 10)
91+
end)
92+
93+
it("GloveBaseTypeTransform: Fists of Stone implicit injects Evasion per level into modDB", function()
94+
build.itemsTab:CreateDisplayItemFromRaw([[
95+
New Item
96+
Stocky Mitts
97+
Armour: 10
98+
]])
99+
build.itemsTab:AddDisplayItem()
100+
runCallback("OnFrame")
101+
102+
local baseEvasion = build.calcsTab.mainOutput.Evasion or 0
103+
104+
build.configTab.input.customMods = "\z
105+
Gloves you equip have their base type transformed to fists of stone while equipped\n\z
106+
"
107+
build.configTab:BuildModList()
108+
runCallback("OnFrame")
109+
110+
-- Implicit: +2 Evasion per level; at level 1 that is +2, plus Fists of Stone base evasion (40)
111+
local transformedEvasion = build.calcsTab.mainOutput.Evasion or 0
112+
assert.is_true(transformedEvasion > baseEvasion,
113+
("expected evasion %d > base evasion %d after Fists of Stone transform"):format(transformedEvasion, baseEvasion))
114+
end)
115+
116+
-- CalcPerform: scoped attribute requirement ignore
117+
118+
it("IgnoreAttributeRequirementsForGloves does not zero global attribute requirements", function()
119+
-- The scoped flag should NOT zero requirements from non-glove sources
120+
build.configTab.input.customMods = "\z
121+
Ignore attribute requirements to equip gloves\n\z
122+
"
123+
build.configTab:BuildModList()
124+
runCallback("OnFrame")
125+
126+
-- Global flag should be nil; only the scoped flag should be set
127+
local globalFlag = build.calcsTab.mainEnv.modDB:Flag(nil, "IgnoreAttributeRequirements")
128+
assert.is_falsy(globalFlag)
129+
assert.is_true(build.calcsTab.mainEnv.modDB:Flag(nil, "IgnoreAttributeRequirementsForGloves"))
130+
end)
131+
end)

src/Data/Bases/gloves.lua

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,5 +889,15 @@ itemBases["Grand Manchettes"] = {
889889
armour = { Armour = 44, Evasion = 40, EnergyShield = 15, },
890890
req = { level = 65, str = 32, dex = 32, int = 32, },
891891
}
892+
itemBases["Fists of Stone"] = {
893+
type = "Gloves",
894+
subType = "Armour/Evasion/Energy Shield",
895+
quality = 20,
896+
socketLimit = 3,
897+
tags = { armour = true, default = true, gloves = true, str_dex_int_armour = true, },
898+
implicitModTypes = { },
899+
armour = { Armour = 44, Evasion = 40, EnergyShield = 15, },
900+
req = { },
901+
}
892902

893903

src/Data/ModCache.lua

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4478,9 +4478,9 @@ c["Glorifying the defilement of 8000 souls in tribute to Kulemak"]={{[1]={flags=
44784478
c["Glorifying the defilement of 8000 souls in tribute to Kurgal"]={{[1]={flags=0,keywordFlags=0,name="JewelData",type="LIST",value={key="conqueredBy",value={conqueror={id=3,type="abyss"},id=8000}}}},nil}
44794479
c["Glorifying the defilement of 8000 souls in tribute to Tecrod"]={{[1]={flags=0,keywordFlags=0,name="JewelData",type="LIST",value={key="conqueredBy",value={conqueror={id=4,type="abyss"},id=8000}}}},nil}
44804480
c["Glorifying the defilement of 8000 souls in tribute to Ulaman"]={{[1]={flags=0,keywordFlags=0,name="JewelData",type="LIST",value={key="conqueredBy",value={conqueror={id=5,type="abyss"},id=8000}}}},nil}
4481-
c["Gloves you equip have their Base Type transformed to Fists of Stone while equipped, and"]={nil,"Gloves you equip have their Base Type transformed to Fists of Stone while equipped, and "}
4482-
c["Gloves you equip have their Base Type transformed to Fists of Stone while equipped, and their Explicit Modifiers are transformed into more powerful related Modifiers"]={nil,"Gloves you equip have their Base Type transformed to Fists of Stone while equipped, and their Explicit Modifiers are transformed into more powerful related Modifiers "}
4483-
c["Gloves you equip have their Base Type transformed to Fists of Stone while equipped, and their Explicit Modifiers are transformed into more powerful related Modifiers Ignore Attribute Requirements to equip Gloves"]={nil,"Gloves you equip have their Base Type transformed to Fists of Stone while equipped, and their Explicit Modifiers are transformed into more powerful related Modifiers Ignore Attribute Requirements to equip Gloves "}
4481+
c["Gloves you equip have their Base Type transformed to Fists of Stone while equipped, and"]={{[1]={flags=0,keywordFlags=0,name="GloveBaseTypeTransform",type="FLAG",value=true}},nil}
4482+
c["Gloves you equip have their Base Type transformed to Fists of Stone while equipped, and their Explicit Modifiers are transformed into more powerful related Modifiers"]={{[1]={flags=0,keywordFlags=0,name="GloveBaseTypeTransform",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="GloveExplicitModTransform",type="FLAG",value=true}},nil}
4483+
c["Gloves you equip have their Base Type transformed to Fists of Stone while equipped, and their Explicit Modifiers are transformed into more powerful related Modifiers Ignore Attribute Requirements to equip Gloves"]={{[1]={flags=0,keywordFlags=0,name="GloveBaseTypeTransform",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="GloveExplicitModTransform",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="IgnoreAttributeRequirementsForGloves",type="FLAG",value=true}},nil}
44844484
c["Grants 1 Passive Skill Point"]={{[1]={flags=0,keywordFlags=0,name="ExtraPoints",type="BASE",value=1}},nil}
44854485
c["Grants 1 additional Skill Slot"]={{[1]={flags=0,keywordFlags=0,name="SkillSlots",type="BASE",value=1}},nil}
44864486
c["Grants 4 Passive Skill Points"]={{[1]={flags=0,keywordFlags=0,name="ExtraPoints",type="BASE",value=4}},nil}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
-- Export script: ModEquivalencies
2+
-- Reads ModEquivalencies.datc64 and writes src/Data/ModEquivalencies.lua
3+
-- Run via the PoB export tool (same way as other Scripts/*.lua files)
4+
--
5+
-- ModEquivalencies maps each source mod ID to its upgraded equivalent.
6+
-- Used by Way of the Stonefist (CalcPerform) to transform glove explicit mods.
7+
--
8+
-- Expected datc64 schema (PoE2):
9+
-- ModsKey : ref to Mods (the source mod being replaced)
10+
-- EquivalentModsKey: ref to Mods (the upgraded replacement mod)
11+
12+
local out = io.open("../Data/ModEquivalencies.lua", "w")
13+
out:write("-- This file is automatically generated, do not edit!\n")
14+
out:write("-- Item data (c) Grinding Gear Games\n\n")
15+
out:write("-- Maps source mod ID -> upgraded equivalent mod ID\n")
16+
out:write("-- Used by Way of the Stonefist to transform glove explicit modifiers.\n")
17+
out:write("return {\n")
18+
19+
local count = 0
20+
for row in dat("ModEquivalencies"):Rows() do
21+
-- The source mod and its upgraded equivalent.
22+
-- Field names reflect the PoE2 datc64 schema; if extraction fails,
23+
-- check the schema in the export tool and update field names below.
24+
local srcMod = row.ModsKey
25+
local dstMod = row.EquivalentModsKey
26+
27+
if srcMod and dstMod and srcMod.Id and dstMod.Id then
28+
out:write('\t["', srcMod.Id, '"] = "', dstMod.Id, '",\n')
29+
count = count + 1
30+
end
31+
end
32+
33+
out:write("}\n")
34+
out:close()
35+
36+
ConPrintf("ModEquivalencies: exported %d entries to ../Data/ModEquivalencies.lua", count)
37+
if count == 0 then
38+
ConPrintf("WARNING: 0 entries written. Check field names ModsKey/EquivalentModsKey match the datc64 schema.")
39+
ConPrintf("Run: dat('ModEquivalencies'):Rows() and inspect row fields manually if needed.")
40+
end

src/Modules/CalcPerform.lua

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1701,6 +1701,7 @@ function calcs.perform(env, skipEHP)
17011701
end
17021702
end
17031703
local ignoreAttrReq = modDB:Flag(nil, "IgnoreAttributeRequirements")
1704+
local ignoreGlovesAttrReq = modDB:Flag(nil, "IgnoreAttributeRequirementsForGloves")
17041705
local strengthSatisfiesMeleeFlag = modDB:Flag(nil, "StrengthSatisfiesMeleeWeaponsAndSkills")
17051706
for _, attr in ipairs(attrTable) do
17061707
local breakdownAttr = attr
@@ -1736,7 +1737,10 @@ function calcs.perform(env, skipEHP)
17361737
and ((reqSource.source == "Item" and reqSource.sourceItem.base.weapon and env.data.weaponTypeInfo[reqSource.sourceItem.base.type].melee)
17371738
or (reqSource.source == "Gem" and reqSource.sourceGem.gemData.tags.melee))
17381739
local satisfyingAttributeValue = gemAttributeRequirementsSatisfiedByHighestAttribute and reqSource.source == "Gem" and highestAttributeValue or out.val
1739-
if req > (strengthSatisfiesMelee and attr ~= "Str" and m_max(satisfyingAttributeValue, output["Str"]) or satisfyingAttributeValue) then
1740+
local ignoreThisReq = ignoreGlovesAttrReq
1741+
and reqSource.source == "Item"
1742+
and reqSource.sourceSlot == "Gloves"
1743+
if not ignoreThisReq and req > (strengthSatisfiesMelee and attr ~= "Str" and m_max(satisfyingAttributeValue, output["Str"]) or satisfyingAttributeValue) then
17401744
out.val = req
17411745
out.source = reqSource
17421746
end
@@ -3173,6 +3177,41 @@ function calcs.perform(env, skipEHP)
31733177
enemyDB:NewMod("DamageTaken", "INC", enemyDB:Sum("INC", nil, "DamageTakenConsecratedGround") * effect, "Consecrated Ground")
31743178
end
31753179

3180+
-- Way of the Stonefist: transform glove base type for this calc pass only
3181+
if modDB:Flag(nil, "GloveBaseTypeTransform") then
3182+
local gloveItem = env.player.itemList["Gloves"]
3183+
if gloveItem and gloveItem.armourData and gloveItem.baseName ~= "Fists of Stone" then
3184+
local fistsOfStone = env.data.itemBases["Fists of Stone"]
3185+
if fistsOfStone then
3186+
local qualityMult = 1 + (gloveItem.quality or 0) / 100
3187+
local origBase = gloveItem.base
3188+
local origBaseName = gloveItem.baseName
3189+
local origArmour = gloveItem.armourData.Armour
3190+
local origEvasion = gloveItem.armourData.Evasion
3191+
local origES = gloveItem.armourData.EnergyShield
3192+
gloveItem.base = fistsOfStone
3193+
gloveItem.baseName = "Fists of Stone"
3194+
gloveItem.armourData.Armour = m_floor((fistsOfStone.armour.Armour or 0) * qualityMult)
3195+
gloveItem.armourData.Evasion = m_floor((fistsOfStone.armour.Evasion or 0) * qualityMult)
3196+
gloveItem.armourData.EnergyShield = m_floor((fistsOfStone.armour.EnergyShield or 0) * qualityMult)
3197+
-- Inject Fists of Stone implicit mods (per-level scaling); modDB is ephemeral per calc pass
3198+
modDB:NewMod("Evasion", "BASE", 2, "Fists of Stone Implicit", { type = "Multiplier", var = "Level" })
3199+
modDB:NewMod("EnergyShield", "BASE", 1, "Fists of Stone Implicit", { type = "Multiplier", var = "Level" })
3200+
modDB:NewMod("Ward", "BASE", 1, "Fists of Stone Implicit", { type = "Multiplier", var = "Level" })
3201+
env.stonefistRestore = {
3202+
item = gloveItem,
3203+
base = origBase, baseName = origBaseName,
3204+
Armour = origArmour, Evasion = origEvasion, EnergyShield = origES
3205+
}
3206+
end
3207+
end
3208+
end
3209+
3210+
-- Way of the Stonefist: explicit mod transformation (requires ModEquivalencies data)
3211+
-- GloveExplicitModTransform is not yet implemented; the mapping data must be generated
3212+
-- by running src/Export/Scripts/modequivalencies.lua via the PoB export tool first.
3213+
-- When src/Data/ModEquivalencies.lua exists, load it here and remap glove explicit mods.
3214+
31763215
-- Defence/offence calculations
31773216
calcs.defence(env, env.player)
31783217
local function getSkillExposureEffect(source, element)
@@ -3422,4 +3461,15 @@ function calcs.perform(env, skipEHP)
34223461

34233462
-- Cache skill data
34243463
cacheData(cacheSkillUUID(env.player.mainSkill, env), env)
3464+
3465+
-- Restore glove item mutated by GloveBaseTypeTransform
3466+
if env.stonefistRestore then
3467+
local r = env.stonefistRestore
3468+
r.item.base = r.base
3469+
r.item.baseName = r.baseName
3470+
r.item.armourData.Armour = r.Armour
3471+
r.item.armourData.Evasion = r.Evasion
3472+
r.item.armourData.EnergyShield = r.EnergyShield
3473+
env.stonefistRestore = nil
3474+
end
34253475
end

src/Modules/ModParser.lua

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@ local modNameList = {
241241
["energy shield"] = "EnergyShield",
242242
["ward"] = "Ward",
243243
["runic ward"] = "Ward",
244+
["maximum ward"] = "Ward",
245+
["maximum runic ward"] = "Ward",
244246
["armour and evasion"] = "ArmourAndEvasion",
245247
["armour and evasion rating"] = "ArmourAndEvasion",
246248
["evasion rating and armour"] = "ArmourAndEvasion",
@@ -1199,6 +1201,7 @@ local preFlagList = {
11991201
["^melee weapon damage"] = { flags = ModFlag.WeaponMelee },
12001202
["^deal "] = { },
12011203
["^causes "] = { },
1204+
["^has "] = { },
12021205
["^arrows deal "] = { keywordFlags = KeywordFlag.Arrow },
12031206
["^critical hits deal "] = { tag = { type = "Condition", var = "CriticalStrike" } },
12041207
["^poisons you inflict with critical hits have "] = { keywordFlags = bor(KeywordFlag.Poison, KeywordFlag.MatchAll), tag = { type = "Condition", var = "CriticalStrike" } },
@@ -1456,6 +1459,7 @@ local modTagList = {
14561459
["per mana burn, up to a maximum of (%d+)%%"] = function(num) return { tag = { type = "Multiplier", var = "ManaBurnStacks", limit = tonumber(num), limitTotal = true } } end,
14571460
["per level"] = { tag = { type = "Multiplier", var = "Level" } },
14581461
["per (%d+) player levels"] = function(num) return { tag = { type = "Multiplier", var = "Level", div = num } } end,
1462+
["per player level"] = { tag = { type = "Multiplier", var = "Level" } },
14591463
["per defiance"] = { tag = { type = "Multiplier", var = "Defiance" } },
14601464
["per (%d+)%% (%a+) effect on enemy"] = function(num, _, effectName) return { tag = { type = "Multiplier", var = firstToUpper(effectName) .. "Effect", div = num, actor = "enemy" } } end,
14611465
["per socketed rune or soul core"] = { tag = { type = "Multiplier", var = "RunesSocketedIn{SlotName}" } },
@@ -2480,6 +2484,9 @@ local specialModList = {
24802484
["critical hits inflict scorch, brittle and sapped"] = { flag("CritAlwaysAltAilments") },
24812485
["you take (%d+)%% of damage from blocked hits"] = function(num) return { mod("BlockEffect", "BASE", num) } end,
24822486
["ignore attribute requirements"] = { flag("IgnoreAttributeRequirements") },
2487+
["ignore attribute requirements to equip gloves"] = { flag("IgnoreAttributeRequirementsForGloves") },
2488+
["gloves you equip have their base type transformed to fists of stone while equipped"] = { flag("GloveBaseTypeTransform") },
2489+
["their explicit modifiers are transformed into more powerful related modifiers"] = { flag("GloveExplicitModTransform") },
24832490
["gain no inherent bonuses from attributes"] = { flag("NoAttributeBonuses") },
24842491
["gain no inherent bonuses from strength"] = { flag("NoStrengthAttributeBonuses") },
24852492
["gain no inherent bonuses from dexterity"] = { flag("NoDexterityAttributeBonuses") },

0 commit comments

Comments
 (0)