Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
8649697
Berek's Grip
Blitz54 Jun 8, 2026
59d856e
Berek's Pass
Blitz54 Jun 8, 2026
c758e91
Berek's Respite
Blitz54 Jun 8, 2026
a65a6b8
Brutus' Lead Sprinkler
Blitz54 Jun 8, 2026
f1fce93
Cat O' Nine Tails
Blitz54 Jun 8, 2026
1ab439c
Decree of Acuity
Blitz54 Jun 8, 2026
79c83d0
Decree of Flight
Blitz54 Jun 8, 2026
089ae86
Decree of Loyalty
Blitz54 Jun 8, 2026
9224aee
Duality
Blitz54 Jun 8, 2026
1735066
Eventide Petals
Blitz54 Jun 8, 2026
c0e5b2e
Eyes of the Runefather
Blitz54 Jun 8, 2026
115499f
Forgotten Warden
Blitz54 Jun 8, 2026
6fd12d8
Geofri's Sanctuary
Blitz54 Jun 8, 2026
5392555
Horror's Flight
Blitz54 Jun 8, 2026
bec1250
Immaculate Adherence
Blitz54 Jun 8, 2026
476569f
Ironbound
Blitz54 Jun 8, 2026
e54ef02
Liminal Coil
Blitz54 Jun 8, 2026
1f65c07
Nightfall
Blitz54 Jun 8, 2026
e8518f8
Opportunity
Blitz54 Jun 8, 2026
2c718a8
Periphery
Blitz54 Jun 8, 2026
93cb190
Redemption
Blitz54 Jun 8, 2026
0b9fb3a
Runeseeker's Call
Blitz54 Jun 8, 2026
85a0a44
Sadist's Mercy
Blitz54 Jun 8, 2026
a502973
Serle's Grit
Blitz54 Jun 8, 2026
6611876
Spiteful Floret
Blitz54 Jun 8, 2026
2c3e300
league name fixes
Blitz54 Jun 8, 2026
0f3137a
Surge of the Tide
Blitz54 Jun 8, 2026
27db5f5
Sylvan's Effigy
Blitz54 Jun 8, 2026
2b0348c
The Auspex
Blitz54 Jun 8, 2026
749ffb4
The Ordained
Blitz54 Jun 8, 2026
280b392
The Raven's Flock
Blitz54 Jun 8, 2026
3bd665d
The Sunken Vessel
Blitz54 Jun 8, 2026
5b18147
The Taming
Blitz54 Jun 8, 2026
d579232
The Unleashed
Blitz54 Jun 8, 2026
3f0932f
Twisted Empyrean
Blitz54 Jun 8, 2026
4312916
Uhtred's Chalice
Blitz54 Jun 8, 2026
89ccbe7
Veilpiercer
Blitz54 Jun 8, 2026
72d8647
Vestige of Darkness
Blitz54 Jun 8, 2026
c1e45a8
Voices
Blitz54 Jun 8, 2026
1f886f6
Mageblood (not supported, just added to pob)
Blitz54 Jun 8, 2026
e63c4e6
Loreweave
Blitz54 Jun 8, 2026
8f8843f
Fix mod ordering on loreweave
Blitz54 Jun 8, 2026
2914f92
Modcache
Blitz54 Jun 9, 2026
c55772e
League for facebreaker
Blitz54 Jun 9, 2026
0d24a2d
Auspex - deflect chance is lucky
Blitz54 Jun 9, 2026
b961336
Fix Spiteful Floret
Blitz54 Jun 9, 2026
fcd406f
Runeseeker's Call - Rune only mod
Blitz54 Jun 9, 2026
bcdff93
Serle's Grit - quality display mod
Blitz54 Jun 9, 2026
dabaf29
Geofri's Sanctuary - life regen per 10 int
Blitz54 Jun 9, 2026
2f050f4
Decree of Acuity - The Effect of Blind on you is reversed
Blitz54 Jun 9, 2026
5a6e703
Spire of Ire - gem name changed in 0.3
Blitz54 Jun 9, 2026
265341a
Fury of the King - bear skills convert
Blitz54 Jun 9, 2026
fa1f6cc
Mageblood - Legacy mods and scaling support
Blitz54 Jun 9, 2026
5eb0f59
improve mod parser and floor mod value
Blitz54 Jun 10, 2026
d385044
Fix topaz mageblood mod
Jun 10, 2026
1054f0d
Merge branch 'dev' into new-uniques-0.5
Jun 10, 2026
f026305
Fix parsing for runeseekers call
Jun 11, 2026
e543733
Fix item paste from game for scaled rune mods
Jun 11, 2026
bb93848
Scale rune mod lines visually
Jun 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 131 additions & 1 deletion spec/System/TestItemParse_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,136 @@ describe("TestItemParse", function()
assert.are.equals("Bonded: +20 to maximum Mana", item.runeModLines[3].line)
end)

it("applies increased effect of socketed runes", function()
local item = new("Item", [[
Test Wand
Runic Fork
Sockets: S
Rune: Lesser Desert Rune
Implicits: 1
{enchant}{rune}Gain 6% of Damage as Extra Fire Damage
200% increased effect of Socketed Runes
]])
item:BuildAndParseRaw()

local damageGainAsFire = 0
for _, mod in ipairs(item.slotModList[1]) do
if mod.name == "DamageGainAsFire" and mod.type == "BASE" then
damageGainAsFire = damageGainAsFire + mod.value
end
end
assert.are.equals(18, damageGainAsFire)
assert.is_not_nil(item:BuildRaw():match("{enchant}{rune}Gain 18%% of Damage as Extra Fire Damage"))
end)

it("does not double-scale imported socketed rune text", function()
local item = new("Item", [[
Runeseeker's Call
Runic Fork
Unique ID: bbcd083b0a9da5650f3ac0a001364b1c99d6b866c1f52f0568fafab863b44ccb
Item Level: 86
Quality: 20
Sockets: S S S S S S
Rune: Hedgewitch Assandra's Rune of Wisdom
Rune: Saqawal's Rune of the Sky
Rune: Perfect Iron Rune
Rune: Perfect Iron Rune
Rune: Perfect Vision Rune
Rune: Legacy of Lifesprig
LevelReq: 90
Implicits: 11
{enchant}{rune}210% increased Spell Damage
{enchant}{rune}+9 to Level of all Spell Skills
{enchant}{rune}84% increased Critical Hit Chance for Spells
{enchant}{rune}Gain 15% of Damage as Extra Damage of all Elements
{enchant}{rune}Bonded: 75% increased Critical Damage Bonus
{enchant}{rune}Bonded: 36% chance when collecting an Elemental Infusion to gain an
{enchant}{rune}additional Elemental Infusion of the same type
{enchant}{rune}Bonded: Archon recovery period expires 90% faster
{enchant}{rune}Bonded: Break Armour on Critical Hit with Spells equal to 72% of Physical Damage dealt
{enchant}{rune}Bonded: Leeches 3% of maximum Life when you Cast a Spell
Grants Skill: Level 20 The Stars Answer
Only Runes can be Socketed in this item
200% increased effect of Socketed Runes
Corrupted
]])
item:BuildAndParseRaw()

local spellDamage = 0
for _, mod in ipairs(item.slotModList[1]) do
if mod.name == "Damage" and mod.type == "INC" and mod.flags == ModFlag.Spell then
spellDamage = spellDamage + mod.value
end
end
assert.are.equals(210, spellDamage)
local rawItem = item:BuildRaw()
assert.is_not_nil(rawItem:match("{enchant}{rune}210%% increased Spell Damage"))
assert.is_not_nil(rawItem:match("{enchant}{rune}%+9 to Level of all Spell Skills"))
end)

it("infers pasted game rune lines with socketed rune effect", function()
local item = new("Item", [[
Item Class: Wands
Rarity: Unique
Runeseeker's Call
Runic Fork
--------
Quality: +20% (augmented)
--------
Requires: Level 90 (unmet)
--------
Sockets: S S S S S
--------
Item Level: 86
--------
Gain 120% of Damage as Extra Lightning Damage (rune)
Remnants you create have 75% reduced effect (rune)
Remnants can be collected from 150% further away (rune)
--------
Grants Skill: Level 20 The Stars Answer
--------
{ Unique Modifier }
Only Runes can be Socketed in this item — Unscalable Value
{ Unique Modifier }
200% increased effect of Socketed Runes — Unscalable Value
--------
Smithed from ancient metal
wrought from the very stars.
It is a means to call upon them,
for one capable of wielding it.
--------
Corrupted
]])

local damageGainAsLightning = 0
for _, mod in ipairs(item.slotModList[1]) do
if mod.name == "DamageGainAsLightning" and mod.type == "BASE" then
damageGainAsLightning = damageGainAsLightning + mod.value
end
end
assert.are.equals(120, damageGainAsLightning)

item:BuildAndParseRaw()

assert.are.equals(5, item.itemSocketCount)
assert.are.equals(5, #item.runes)
for _, rune in ipairs(item.runes) do
assert.are_not.equals("None", rune)
end

damageGainAsLightning = 0
for _, mod in ipairs(item.slotModList[1]) do
if mod.name == "DamageGainAsLightning" and mod.type == "BASE" then
damageGainAsLightning = damageGainAsLightning + mod.value
end
end
assert.are.equals(120, damageGainAsLightning)
local rawItem = item:BuildRaw()
assert.is_not_nil(rawItem:match("{enchant}{rune}Gain 120%% of Damage as Extra Lightning Damage"))
assert.is_not_nil(rawItem:match("{enchant}{rune}Remnants you create have 75%% reduced effect"))
assert.is_not_nil(rawItem:match("{enchant}{rune}Remnants can be collected from 150%% further away"))
end)

it("multi-line rune mod", function()
-- Thruldana is Bow-only as well
local item = new("Item", [[
Expand Down Expand Up @@ -697,4 +827,4 @@ describe("TestAdvancedItemParse #item", function()
Note: ~b/o 2 chaos
]])
end)
end)
end)
96 changes: 85 additions & 11 deletions src/Classes/Item.lua
Original file line number Diff line number Diff line change
Expand Up @@ -850,8 +850,8 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
gameModeStage = "IMPLICIT"
end
local catalystScalar = 1
if line:match(" %- Unscalable Value$") then
line = line:gsub(" %- Unscalable Value$", "")
if line:match(" %- Unscalable Value$") or line:match(" — Unscalable Value$") then
line = line:gsub(" %- Unscalable Value$", ""):gsub(" — Unscalable Value$", "")
modLine.unscalable = true
else
catalystScalar = getCatalystScalar(self.catalyst, modLine, self.catalystQuality)
Expand Down Expand Up @@ -1014,6 +1014,18 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
if self.base then
if self.base.weapon or self.base.armour or self.base.tags.wand or self.base.tags.staff or self.base.tags.sceptre then
local shouldFixRunesOnItem = #self.runes == 0
if not shouldFixRunesOnItem and #self.runeModLines > 0 then
local canRebuildRunes = true
for _, rune in ipairs(self.runes) do
if rune ~= "None" and not data.itemMods.Runes[rune] then
canRebuildRunes = false
break
end
end
if canRebuildRunes then
self:UpdateRunes()
end
end

local function getRuneLineParts(modLine)
local values = { }
Expand Down Expand Up @@ -1105,17 +1117,17 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
local broadItemType = self.base.weapon and "weapon" or (self.base.tags.wand or self.base.tags.staff) and "caster" or "armour" -- minor optimisation
local specificItemType = self.base.type:lower()
for runeName, runeMods in pairs(data.itemMods.Runes) do
local addModToGroupedRunes = function (modLine)
local addModToGroupedRunes = function (modLine, augmentType)
local runeStrippedModLine, runeValues = getRuneLineParts(modLine)
if statGroupedRunes[runeStrippedModLine] == nil then
statGroupedRunes[runeStrippedModLine] = { }
end
t_insert(statGroupedRunes[runeStrippedModLine], { name = runeName, values = runeValues })
t_insert(statGroupedRunes[runeStrippedModLine], { name = runeName, type = augmentType, values = runeValues })
end
for slotType, slotMod in pairs(runeMods) do
if slotType == broadItemType or slotType == specificItemType then
for _, mod in ipairs(slotMod) do
addModToGroupedRunes(mod)
addModToGroupedRunes(mod, slotMod.type)
end
end
end
Expand All @@ -1126,19 +1138,45 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
table.sort(runes, function(a, b) return compareRuneValueSets(a.values, b.values) end)
end

local gameSocketedRuneEffectModifier = 0
if mode == "GAME" and shouldFixRunesOnItem then
for _, modLines in ipairs({ self.enchantModLines, self.implicitModLines, self.explicitModLines }) do
for _, effectModLine in ipairs(modLines) do
for _, mod in ipairs(effectModLine.modList or { }) do
if mod.name == "SocketedRuneEffect" and mod.type == "INC" then
gameSocketedRuneEffectModifier = gameSocketedRuneEffectModifier + mod.value / 100
end
end
end
end
end

local remainingRunes = self.itemSocketCount
for i, modLine in ipairs(self.runeModLines) do
local strippedModLine, targetValues = getRuneLineParts(modLine.line)
local groupedRunes = statGroupedRunes[strippedModLine]
if groupedRunes and not modLine.bonded then -- found the rune category with the relevant stat.
local result, numRunes = findRuneCombination(groupedRunes, targetValues, remainingRunes)
local result, numRunes
local socketedRuneEffectAlreadyApplied
if gameSocketedRuneEffectModifier ~= 0 then
local unscaledTargetValues = { }
for valueIndex, value in ipairs(targetValues) do
unscaledTargetValues[valueIndex] = value / (1 + gameSocketedRuneEffectModifier)
end
result, numRunes = findRuneCombination(groupedRunes, unscaledTargetValues, remainingRunes)
socketedRuneEffectAlreadyApplied = result ~= nil
end
if not result then
result, numRunes = findRuneCombination(groupedRunes, targetValues, remainingRunes)
end

if result then -- we have found a valid combo for that rune category
remainingRunes = remainingRunes - numRunes
-- this code should probably be refactored to based off stored self.runes rather than the recomputed amounts off the runeModLines this
-- is too avoid having to run the relatively expensive recomputation every time the item is parsed even if we know the runes on the item already.
modLine.soulCore = groupedRunes[1].name:match("Soul Core") ~= nil
modLine.augmentType = groupedRunes[1].type
modLine.runeCount = numRunes
modLine.socketedRuneEffectAlreadyApplied = socketedRuneEffectAlreadyApplied

if shouldFixRunesOnItem then
for index, rune in ipairs(groupedRunes) do
Expand Down Expand Up @@ -1286,6 +1324,9 @@ end

function ItemClass:BuildRaw()
local rawLines = { }
if self.runeModLines and self.runeModLines[1] then
self:ApplySocketedRuneDisplayScalars()
end
t_insert(rawLines, "Rarity: " .. self.rarity)
if self.title then
t_insert(rawLines, self.title)
Expand Down Expand Up @@ -1345,7 +1386,8 @@ function ItemClass:BuildRaw()
t_insert(rawLines, "Item Level: " .. self.itemLevel)
end
local function writeModLine(modLine)
local line = modLine.line
local displayValueScalar = modLine.displayValueScalar and (modLine.valueScalar or 1) * modLine.displayValueScalar
local line = displayValueScalar and itemLib.applyRange(modLine.line, modLine.range or main.defaultItemAffixQuality, displayValueScalar, modLine.corruptedRange) or modLine.line
if modLine.range and line:match("%(%-?[%d%.]+%-%-?[%d%.]+%)") then
line = "{range:" .. round(modLine.range, 3) .. "}" .. line
end
Expand Down Expand Up @@ -1526,7 +1568,7 @@ function ItemClass:UpdateRunes()
for _, mod in ipairs(gatheredMods) do
for i, modLine in ipairs(mod) do
local order = mod.statOrder[i]
local orderKey = modLine:match("^Bonded:") and "Bonded:"..order or order
local orderKey = mod.type .. ":" .. (modLine:match("^Bonded:") and "Bonded:"..order or order)
if statOrder[orderKey] then
-- Combine stats
local start = 1
Expand All @@ -1535,8 +1577,12 @@ function ItemClass:UpdateRunes()
start = e + 1
return tonumber(num) + tonumber(other)
end)
local modList, extra = modLib.parseMod(statOrder[orderKey].line)
statOrder[orderKey].modList = modList or { }
statOrder[orderKey].extra = extra
else
local modLine = { line = modLine, order = order, rune = true, enchant = true }
local modList, extra = modLib.parseMod(modLine)
local modLine = { line = modLine, order = order, modList = modList or { }, extra = extra, rune = true, enchant = true, augmentType = mod.type }
for l = 1, #self.runeModLines + 1 do
if not self.runeModLines[l] or self.runeModLines[l].order > order then
t_insert(self.runeModLines, l, modLine)
Expand All @@ -1551,6 +1597,18 @@ function ItemClass:UpdateRunes()
end
end

function ItemClass:ApplySocketedRuneDisplayScalars()
for _, modLine in ipairs(self.runeModLines or { }) do
local effectModifier = modLine.augmentType == "SoulCore" and (self.socketedSoulCoreEffectModifier or 0)
or modLine.augmentType == "Rune" and (self.socketedRuneEffectModifier or 0)
if effectModifier and effectModifier ~= 0 and not modLine.socketedRuneEffectAlreadyApplied then
modLine.displayValueScalar = 1 + effectModifier
else
modLine.displayValueScalar = nil
end
end
end

-- Rebuild explicit modifiers using the item's affixes
function ItemClass:Craft()
-- Save off any custom mods so they can be re-added at the end
Expand Down Expand Up @@ -2020,6 +2078,20 @@ function ItemClass:BuildModList()
for _, modLine in ipairs(self.explicitModLines) do
processModLine(modLine)
end
self.socketedSoulCoreEffectModifier = calcLocal(baseList, "SocketedSoulCoreEffect", "INC", 0) / 100
self.socketedRuneEffectModifier = calcLocal(baseList, "SocketedRuneEffect", "INC", 0) / 100
if self.runeModLines[1] then
self:ApplySocketedRuneDisplayScalars()
end
for _, modLine in ipairs(self.runeModLines) do
local effectModifier = modLine.augmentType == "SoulCore" and self.socketedSoulCoreEffectModifier
or modLine.augmentType == "Rune" and self.socketedRuneEffectModifier
if effectModifier and effectModifier ~= 0 and self:CheckModLineVariant(modLine) and not modLine.extra and not modLine.socketedRuneEffectAlreadyApplied then
for _, mod in ipairs(modLine.modList) do
baseList:ScaleAddMod(mod, effectModifier)
end
end
end
self.grantedSkills = { }
for _, skill in ipairs(baseList:List(nil, "ExtraSkill")) do
if skill.name ~= "Unknown" then
Expand Down Expand Up @@ -2085,6 +2157,9 @@ function ItemClass:BuildModList()
baseList:NewMod("ArmourData", "LIST", { key = "EnergyShield", value = 0 })
self.requirements.int = 0
end
if self.name == "Geofri's Sanctuary, Revered Vestments" then
baseList:NewMod("ArmourData", "LIST", { key = "EnergyShield", value = 0 })
end
if calcLocal(baseList, "NoAttributeRequirements", "FLAG", 0) then
self.requirements.strMod = 0
self.requirements.dexMod = 0
Expand Down Expand Up @@ -2126,5 +2201,4 @@ function ItemClass:BuildModList()
else
self.modList = self:BuildModListForSlotNum(baseList)
end
self.socketedSoulCoreEffectModifier = calcLocal(baseList, "SocketedSoulCoreEffect", "INC", 0) / 100
end
18 changes: 11 additions & 7 deletions src/Classes/ItemsTab.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1918,6 +1918,14 @@ end)

function ItemsTabClass:GetValidRunesForItem(item)
local runes = { }
local socketedItemType
if item.baseModList then
if item.baseModList:Flag(nil, "SocketedSoulCoresOnly") then
socketedItemType = "SoulCore"
elseif item.baseModList:Flag(nil, "SocketedRunesOnly") then
socketedItemType = "Rune"
end
end
for _, rune in pairs(runeModLines) do
local subType = item.base.subType and item.base.subType:lower()
local itemType = item.base.type:lower()
Expand All @@ -1939,13 +1947,9 @@ function ItemsTabClass:GetValidRunesForItem(item)
end
end
if isRuneValidForSlot(rune.slot) then
if item.title == "Atziri's Splendour" then
if rune.slot == "None" or rune.type == "SoulCore" then
table.insert(runes, rune)
end
else
table.insert(runes, rune)
end
if rune.slot == "None" or not socketedItemType or rune.type == socketedItemType then
table.insert(runes, rune)
end
end
end
return runes
Expand Down
Loading
Loading