@@ -58,6 +58,80 @@ local function getCatalystScalar(catalystId, mod, quality)
5858 return 1
5959end
6060
61+ local function stonefistAverageRanges (text )
62+ return (text :gsub (" %((%-?%d+%.?%d*)%-(%-?%d+%.?%d*)%)" , function (a , b )
63+ return tostring (m_floor ((tonumber (a ) + tonumber (b )) / 2 + 0.5 ))
64+ end ))
65+ end
66+
67+ local function stonefistComputePerLevel (text , displayLevel )
68+ if displayLevel then
69+ local value , label = text :match (" ^Has ([%+%-]?%d+) (.+) per player level$" )
70+ if value then
71+ local total = tonumber (value ) * displayLevel
72+ return (total >= 0 and " +" or " " ) .. total .. " " .. label
73+ end
74+ end
75+ return text
76+ end
77+
78+ local function stonefistSlotify (s )
79+ local slots = { }
80+ local out = { }
81+ local pos , len = 1 , # s
82+ while pos <= len do
83+ local rs , re , a , b = s :find (" ^%((%-?%d+%.?%d*)%-(%-?%d+%.?%d*)%)" , pos )
84+ if rs then
85+ t_insert (slots , { tonumber (a ), tonumber (b ) })
86+ t_insert (out , " #" )
87+ pos = re + 1
88+ else
89+ local ns , ne , n = s :find (" ^(%-?%d+%.?%d*)" , pos )
90+ if ns then
91+ t_insert (slots , { tonumber (n ), tonumber (n ) })
92+ t_insert (out , " #" )
93+ pos = ne + 1
94+ else
95+ t_insert (out , s :sub (pos , pos ))
96+ pos = pos + 1
97+ end
98+ end
99+ end
100+ return table.concat (out ), slots
101+ end
102+
103+ local stonefistBaseName = " Fists of Stone"
104+
105+ local stonefistSourceIndexCache = setmetatable ({ }, { __mode = " k" })
106+ local function getStonefistSourceIndex (map , affixes )
107+ local index = stonefistSourceIndexCache [affixes ]
108+ if index then
109+ return index
110+ end
111+ index = { }
112+ for modId in pairs (map ) do
113+ local modData = affixes [modId ]
114+ if modData then
115+ for i = 1 , # modData do
116+ if type (modData [i ]) == " string" then
117+ local tmpl , slots = stonefistSlotify (modData [i ])
118+ local bucket = index [tmpl ]
119+ if not bucket then
120+ bucket = { }
121+ index [tmpl ] = bucket
122+ end
123+ bucket [# bucket + 1 ] = { id = modId , slots = slots }
124+ end
125+ end
126+ end
127+ end
128+ for _ , bucket in pairs (index ) do
129+ table.sort (bucket , function (a , b ) return a .id < b .id end )
130+ end
131+ stonefistSourceIndexCache [affixes ] = index
132+ return index
133+ end
134+
61135local ItemClass = newClass (" Item" , function (self , raw , rarity , highQuality )
62136 if raw then
63137 self :ParseRaw (sanitiseText (raw ), rarity , highQuality )
@@ -303,6 +377,7 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
303377 self .spiritValue = nil
304378 self .runicItem = nil
305379 self .quality = nil
380+ self .stonefistVariantCache = nil
306381 self .rawLines = { }
307382 -- Find non-blank lines and trim whitespace
308383 for line in raw :gmatch (" %s*([^\n ]*%S)" ) do
@@ -727,6 +802,8 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
727802 modLine .range = tonumber (val )
728803 elseif k == " corruptedRange" then
729804 modLine .corruptedRange = tonumber (val )
805+ elseif k == " modId" then
806+ modLine .modId = val
730807 elseif lineFlags [k ] then
731808 modLine [k ] = true
732809 end
@@ -1402,6 +1479,9 @@ function ItemClass:BuildRaw()
14021479 if modLine .corruptedRange then
14031480 line = " {corruptedRange:" .. round (modLine .corruptedRange , 2 ) .. " }" .. line
14041481 end
1482+ if modLine .modId then
1483+ line = " {modId:" .. modLine .modId .. " }" .. line
1484+ end
14051485 if modLine .rune then
14061486 line = " {rune}" .. line
14071487 end
@@ -1543,6 +1623,120 @@ function ItemClass:BuildAndParseRaw()
15431623 self :ParseRaw (raw )
15441624end
15451625
1626+ function ItemClass :ResolveStonefistModId (modLine )
1627+ local map = data .stonefistMap
1628+ local affixes = self .affixes
1629+ if not map or not affixes then
1630+ return nil
1631+ end
1632+ local index = getStonefistSourceIndex (map , affixes )
1633+ for part in (modLine .line .. " \n " ):gmatch (" (.-)\n " ) do
1634+ local stat = part :gsub (" %s+" , " " ):gsub (" ^%s+" , " " ):gsub (" %s+$" , " " )
1635+ if stat ~= " " then
1636+ local lineTmpl , lineSlots = stonefistSlotify (stat )
1637+ local bucket = index [lineTmpl ]
1638+ if bucket then
1639+ for _ , cand in ipairs (bucket ) do
1640+ local statSlots = cand .slots
1641+ local ok = true
1642+ for j = 1 , # statSlots do
1643+ local v = lineSlots [j ][1 ]
1644+ if v < m_min (statSlots [j ][1 ], statSlots [j ][2 ]) or v > m_max (statSlots [j ][1 ], statSlots [j ][2 ]) then
1645+ ok = false
1646+ break
1647+ end
1648+ end
1649+ if ok then
1650+ return cand .id
1651+ end
1652+ end
1653+ end
1654+ end
1655+ end
1656+ return nil
1657+ end
1658+
1659+ function ItemClass :CreateStonefistVariant (displayLevel )
1660+ local map = data .stonefistMap
1661+ if not map then
1662+ return nil
1663+ end
1664+ local cacheKey = displayLevel or 0
1665+ local cache = self .stonefistVariantCache
1666+ if cache then
1667+ local cached = cache [cacheKey ]
1668+ if cached ~= nil then
1669+ return cached or nil
1670+ end
1671+ else
1672+ cache = { }
1673+ self .stonefistVariantCache = cache
1674+ end
1675+ local clone = new (" Item" , self :BuildRaw ())
1676+ local newExplicit = { }
1677+ local emitted = { }
1678+ local changed = false
1679+ for _ , modLine in ipairs (clone .explicitModLines ) do
1680+ local modId = modLine .modId
1681+ if not (modId and map [modId ]) then
1682+ modId = clone :ResolveStonefistModId (modLine )
1683+ end
1684+ if modId and map [modId ] then
1685+ changed = true
1686+ if not emitted [modId ] then
1687+ emitted [modId ] = true
1688+ local entry = data .itemMods .FistsOfStone [map [modId ]]
1689+ if entry then
1690+ for i = 1 , # entry do
1691+ if type (entry [i ]) == " string" then
1692+ local text = stonefistAverageRanges (entry [i ])
1693+ local list , extra = modLib .parseMod (text )
1694+ t_insert (newExplicit , { line = stonefistComputePerLevel (text , displayLevel ), modList = list or { }, extra = extra })
1695+ end
1696+ end
1697+ end
1698+ end
1699+ else
1700+ t_insert (newExplicit , modLine )
1701+ end
1702+ end
1703+ if clone .base and clone .base .armour then
1704+ -- Runeforged/runemastered (Runic Ward) bases use the Runeforged Fists of Stone implicit.
1705+ local hasWard = clone .runicItem or (clone .base .armour .Ward or 0 ) > 0
1706+ local armourCopy = { }
1707+ for k , v in pairs (clone .base .armour ) do
1708+ armourCopy [k ] = v
1709+ end
1710+ armourCopy .Armour , armourCopy .Evasion , armourCopy .EnergyShield , armourCopy .Ward = nil , nil , nil , nil
1711+ clone .base = setmetatable ({ armour = armourCopy }, { __index = clone .base })
1712+ -- The transformed base has no flat defences; its per-level defence is the base implicit
1713+ -- of the Fists of Stone base (read from the base data so it stays in sync).
1714+ local fistsBase = data .itemBases [hasWard and " Runeforged Fists of Stone" or " Fists of Stone" ]
1715+ if fistsBase and fistsBase .implicit then
1716+ local lines = { }
1717+ for implicitLine in (fistsBase .implicit .. " \n " ):gmatch (" (.-)\n " ) do
1718+ if implicitLine ~= " " then
1719+ t_insert (lines , implicitLine )
1720+ end
1721+ end
1722+ for i = # lines , 1 , - 1 do
1723+ local list , extra = modLib .parseMod (lines [i ])
1724+ t_insert (clone .implicitModLines , 1 , { line = stonefistComputePerLevel (lines [i ], displayLevel ), modList = list or { }, extra = extra })
1725+ end
1726+ end
1727+ changed = true
1728+ end
1729+ if not changed then
1730+ cache [cacheKey ] = false
1731+ return nil
1732+ end
1733+ clone .explicitModLines = newExplicit
1734+ clone :BuildModList ()
1735+ clone .baseName = stonefistBaseName
1736+ cache [cacheKey ] = clone
1737+ return clone
1738+ end
1739+
15461740-- Rebuild rune modifiers using the item's runes
15471741function ItemClass :UpdateRunes ()
15481742 wipeTable (self .runeModLines )
@@ -1666,7 +1860,7 @@ function ItemClass:Craft()
16661860 return tonumber (num ) + tonumber (other )
16671861 end )
16681862 else
1669- local modLine = { line = line , order = order }
1863+ local modLine = { line = line , order = order , modId = affix . modId }
16701864 for l = 1 , # self .explicitModLines + 1 do
16711865 if not self .explicitModLines [l ] or self .explicitModLines [l ].order > order then
16721866 t_insert (self .explicitModLines , l , modLine )
0 commit comments