From 5dd2c3d4cc800a421e2f184da54fa807a183f147 Mon Sep 17 00:00:00 2001 From: jokker23 Date: Sun, 5 Jul 2026 11:55:48 -0600 Subject: [PATCH 1/3] Absent Amulet grants a reservation-free skill When the Absent Amulet grants a reserving skill (Archmage), the skill is provided for free in-game and reserves no Spirit. PoB was still charging the granted skill's full base reservation (100 Spirit for Archmage), overstating reserved Spirit and understating unreserved Spirit for any build using the amulet. --- spec/System/TestBirthedReservation_spec.lua | 50 +++++++++++++++++++++ src/Classes/Item.lua | 1 + src/Data/Bases/amulet.lua | 1 + src/Export/Scripts/bases.lua | 3 ++ src/Modules/CalcDefence.lua | 4 ++ src/Modules/CalcSetup.lua | 1 + 6 files changed, 60 insertions(+) create mode 100644 spec/System/TestBirthedReservation_spec.lua diff --git a/spec/System/TestBirthedReservation_spec.lua b/spec/System/TestBirthedReservation_spec.lua new file mode 100644 index 0000000000..af43b79e89 --- /dev/null +++ b/spec/System/TestBirthedReservation_spec.lua @@ -0,0 +1,50 @@ +describe("TestBirthedReservation", function() + before_each(function() + newBuild() + build.configTab.input.customMods = "+1000 to Spirit" + build.configTab:BuildModList() + end) + + local function grantAmulet(baseName, skillLine) + build.itemsTab:CreateDisplayItemFromRaw("New Item\n"..baseName.."\nImplicits: 1\nGrants Skill: "..skillLine.."\n") + build.itemsTab:AddDisplayItem() + end + + it("Absent Amulet grants a reservation-free skill", function() + grantAmulet("Absent Amulet", "Level 20 Archmage") + runCallback("OnFrame") + assert.are.equals(0, build.calcsTab.mainOutput.SpiritReserved) + end) + + it("other Birthed amulets (Lament, Portent) are not affected and still reserve", function() + grantAmulet("Lament Amulet", "Level 20 Archmage") + runCallback("OnFrame") + assert.are.equals(100, build.calcsTab.mainOutput.SpiritReserved) + + newBuild() + build.configTab.input.customMods = "+1000 to Spirit" + build.configTab:BuildModList() + grantAmulet("Portent Amulet", "Level 20 Archmage") + runCallback("OnFrame") + assert.are.equals(100, build.calcsTab.mainOutput.SpiritReserved) + end) + + it("support gems added to a birthed-granted skill still reserve", function() + grantAmulet("Absent Amulet", "Level 20 Archmage") + build.skillsTab:PasteSocketGroup("Slot: Amulet\nClarity I 1/0 1\n") + runCallback("OnFrame") + assert.are.equals(10, build.calcsTab.mainOutput.SpiritReserved) + end) + + it("a non-birthed item granting the same skill still reserves normally", function() + grantAmulet("Lapis Amulet", "Level 20 Archmage") + runCallback("OnFrame") + assert.are.equals(100, build.calcsTab.mainOutput.SpiritReserved) + end) + + it("a socketed copy of the skill still reserves normally", function() + build.skillsTab:PasteSocketGroup("Archmage 20/0 1\n") + runCallback("OnFrame") + assert.are.equals(100, build.calcsTab.mainOutput.SpiritReserved) + end) +end) diff --git a/src/Classes/Item.lua b/src/Classes/Item.lua index 684f904714..db6d4bfca5 100644 --- a/src/Classes/Item.lua +++ b/src/Classes/Item.lua @@ -2139,6 +2139,7 @@ function ItemClass:BuildModList() skillId = skill.skillId, level = skill.level, noSupports = skill.noSupports, + noReservation = self.base and self.base.grantedSkillsReserveNoSpirit or nil, source = self.modSource, triggered = skill.triggered, triggerChance = skill.triggerChance, diff --git a/src/Data/Bases/amulet.lua b/src/Data/Bases/amulet.lua index be1b2ba358..8aeb525655 100644 --- a/src/Data/Bases/amulet.lua +++ b/src/Data/Bases/amulet.lua @@ -130,6 +130,7 @@ itemBases["Portent Amulet"] = { itemBases["Absent Amulet"] = { type = "Amulet", tags = { amulet = true, default = true, }, + grantedSkillsReserveNoSpirit = true, variantList = { "Cast on Elemental Ailment", "Cast on Critical", "Cast on Dodge", "Rhoa Mount", "Archmage", "Trinity", "Eternal Rage", }, implicit = "-1 Prefix Modifier allowed\n-1 Suffix Modifier allowed\n{variant:1}Grants Skill: Level (1-20) Cast on Elemental Ailment\n{variant:2}Grants Skill: Level (1-20) Cast on Critical\n{variant:3}Grants Skill: Level (1-20) Cast on Dodge\n{variant:4}Grants Skill: Level (1-20) Rhoa Mount\n{variant:5}Grants Skill: Level (1-20) Archmage\n{variant:6}Grants Skill: Level (1-20) Trinity\n{variant:7}Grants Skill: Level (1-20) Eternal Rage", implicitModTypes = { { }, { }, }, diff --git a/src/Export/Scripts/bases.lua b/src/Export/Scripts/bases.lua index 6bc3151bc4..f2d4145c30 100644 --- a/src/Export/Scripts/bases.lua +++ b/src/Export/Scripts/bases.lua @@ -140,6 +140,9 @@ directiveTable.base = function(state, args, out) out:write(tag, ' = true, ') end out:write('},\n') + if baseTypeId == "Metadata/Items/Amulets/FourAmuletB1c" then + out:write('\tgrantedSkillsReserveNoSpirit = true,\n') + end local implicitLines = { } local implicitModTypes = { } local variantList = { } diff --git a/src/Modules/CalcDefence.lua b/src/Modules/CalcDefence.lua index c21c9e199c..35f471e818 100644 --- a/src/Modules/CalcDefence.lua +++ b/src/Modules/CalcDefence.lua @@ -252,6 +252,10 @@ function calcs.doActorLifeManaSpiritReservation(actor) pool.Life.basePercent = pool.Life.basePercent + pool.Spirit.basePercent * spiritToLifeReservation pool.Spirit.basePercent = 0 end + if activeSkill.activeEffect.srcInstance and activeSkill.activeEffect.srcInstance.noReservation then + for _, values in pairs(pool) do values.baseFlat, values.basePercent = 0, 0 end + pool.Spirit.baseFlat = skillModList:Sum("BASE", skillCfg, "ExtraSpirit") + end for name, values in pairs(pool) do values.more = skillModList:More(skillCfg, name.."Reserved", "Reserved") values.inc = skillModList:Sum("INC", skillCfg, name.."Reserved", "Reserved") diff --git a/src/Modules/CalcSetup.lua b/src/Modules/CalcSetup.lua index 38a68d62f1..5ba727778c 100644 --- a/src/Modules/CalcSetup.lua +++ b/src/Modules/CalcSetup.lua @@ -1534,6 +1534,7 @@ function calcs.initEnv(build, mode, override, specEnv) activeGemInstance.enableGlobal1 = true activeGemInstance.noSupports = grantedSkill.noSupports group.noSupports = grantedSkill.noSupports + activeGemInstance.noReservation = grantedSkill.noReservation activeGemInstance.triggered = grantedSkill.triggered activeGemInstance.triggerChance = grantedSkill.triggerChance wipeTable(group.gemList) From 61b408b8e6e4befbf6e80d07fbebc96cae768394 Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Fri, 10 Jul 2026 22:12:34 +1000 Subject: [PATCH 2/3] Fix implementation The implementation now correctly handles supported gems that add spirit costs and gems that convert the cost to a life cost It was also not working with the Lament of Portent Amulet bases and was not using the field from the game files to set the flag on the item base --- spec/System/TestBirthedReservation_spec.lua | 18 +++++++++++++----- src/Classes/Item.lua | 2 +- src/Data/Bases/amulet.lua | 4 +++- src/Export/Scripts/bases.lua | 6 +++--- src/Export/spec.lua | 4 ++-- src/Modules/CalcActiveSkill.lua | 6 ++++++ src/Modules/CalcDefence.lua | 4 ---- 7 files changed, 28 insertions(+), 16 deletions(-) diff --git a/spec/System/TestBirthedReservation_spec.lua b/spec/System/TestBirthedReservation_spec.lua index af43b79e89..565e050d2c 100644 --- a/spec/System/TestBirthedReservation_spec.lua +++ b/spec/System/TestBirthedReservation_spec.lua @@ -16,17 +16,17 @@ describe("TestBirthedReservation", function() assert.are.equals(0, build.calcsTab.mainOutput.SpiritReserved) end) - it("other Birthed amulets (Lament, Portent) are not affected and still reserve", function() - grantAmulet("Lament Amulet", "Level 20 Archmage") + it("other Birthed amulets also grant reservation-free skills", function() + grantAmulet("Lament Amulet", "Level 20 Arctic Armour") runCallback("OnFrame") - assert.are.equals(100, build.calcsTab.mainOutput.SpiritReserved) + assert.are.equals(0, build.calcsTab.mainOutput.SpiritReserved) newBuild() build.configTab.input.customMods = "+1000 to Spirit" build.configTab:BuildModList() - grantAmulet("Portent Amulet", "Level 20 Archmage") + grantAmulet("Portent Amulet", "Level 20 Wolf Pack") runCallback("OnFrame") - assert.are.equals(100, build.calcsTab.mainOutput.SpiritReserved) + assert.are.equals(0, build.calcsTab.mainOutput.SpiritReserved) end) it("support gems added to a birthed-granted skill still reserve", function() @@ -36,6 +36,14 @@ describe("TestBirthedReservation", function() assert.are.equals(10, build.calcsTab.mainOutput.SpiritReserved) end) + it("support reservation conversions still apply", function() + grantAmulet("Absent Amulet", "Level 20 Archmage") + build.skillsTab:PasteSocketGroup("Slot: Amulet\nClarity I 1/0 1\nAtziri's Communion 1/0 1\n") + runCallback("OnFrame") + assert.is_true(build.calcsTab.mainOutput.LifeReservedPercent > 0) + assert.are.equals(0, build.calcsTab.mainOutput.SpiritReserved) + end) + it("a non-birthed item granting the same skill still reserves normally", function() grantAmulet("Lapis Amulet", "Level 20 Archmage") runCallback("OnFrame") diff --git a/src/Classes/Item.lua b/src/Classes/Item.lua index db6d4bfca5..dd34976211 100644 --- a/src/Classes/Item.lua +++ b/src/Classes/Item.lua @@ -2139,7 +2139,7 @@ function ItemClass:BuildModList() skillId = skill.skillId, level = skill.level, noSupports = skill.noSupports, - noReservation = self.base and self.base.grantedSkillsReserveNoSpirit or nil, + noReservation = self.base and self.base.grantedSkillsHaveNoReservation or nil, source = self.modSource, triggered = skill.triggered, triggerChance = skill.triggerChance, diff --git a/src/Data/Bases/amulet.lua b/src/Data/Bases/amulet.lua index 8aeb525655..26ec749e2c 100644 --- a/src/Data/Bases/amulet.lua +++ b/src/Data/Bases/amulet.lua @@ -114,6 +114,7 @@ itemBases["Runemastered Veridical Chain"] = { itemBases["Lament Amulet"] = { type = "Amulet", tags = { amulet = true, default = true, }, + grantedSkillsHaveNoReservation = true, variantList = { "Arctic Armour", "Herald of Ash", "Herald of Ice", "Herald of Thunder", "Plague Bearer", "Barrier Invocation", "Lingering Illusion", "Ghost Dance", "Mana Remnants", "Magma Barrier", "Raging Spirits", "Wind Dancer", "Cast on Minion Death", "Scavenged Plating", "War Banner", "Defiance Banner", "Dread Banner", "Time of Need", "Attrition", "Charge Regulation", "Shard Scavenger", "Overwhelming Presence", "Alchemist's Boon", "Reaper's Invocation", "Berserk", "Herald of Plague", "Herald of Blood", "Withering Presence", "Ravenous Swarm", "Combat Frenzy", "Trail of Caltrops", "Convalescence", "Iron Ward", "Siphon Elements", "Briarpatch", "Savage Fury", "Barkskin", }, implicit = "-1 Prefix Modifier allowed\n{variant:1}Grants Skill: Level (1-20) Arctic Armour\n{variant:2}Grants Skill: Level (1-20) Herald of Ash\n{variant:3}Grants Skill: Level (1-20) Herald of Ice\n{variant:4}Grants Skill: Level (1-20) Herald of Thunder\n{variant:5}Grants Skill: Level (1-20) Plague Bearer\n{variant:6}Grants Skill: Level (1-20) Barrier Invocation\n{variant:7}Grants Skill: Level (1-20) Lingering Illusion\n{variant:8}Grants Skill: Level (1-20) Ghost Dance\n{variant:9}Grants Skill: Level (1-20) Mana Remnants\n{variant:10}Grants Skill: Level (1-20) Magma Barrier\n{variant:11}Grants Skill: Level (1-20) Raging Spirits\n{variant:12}Grants Skill: Level (1-20) Wind Dancer\n{variant:13}Grants Skill: Level (1-20) Cast on Minion Death\n{variant:14}Grants Skill: Level (1-20) Scavenged Plating\n{variant:15}Grants Skill: Level (1-20) War Banner\n{variant:16}Grants Skill: Level (1-20) Defiance Banner\n{variant:17}Grants Skill: Level (1-20) Dread Banner\n{variant:18}Grants Skill: Level (1-20) Time of Need\n{variant:19}Grants Skill: Level (1-20) Attrition\n{variant:20}Grants Skill: Level (1-20) Charge Regulation\n{variant:21}Grants Skill: Level (1-20) Shard Scavenger\n{variant:22}Grants Skill: Level (1-20) Overwhelming Presence\n{variant:23}Grants Skill: Level (1-20) Alchemist's Boon\n{variant:24}Grants Skill: Level (1-20) Reaper's Invocation\n{variant:25}Grants Skill: Level (1-20) Berserk\n{variant:26}Grants Skill: Level (1-20) Herald of Plague\n{variant:27}Grants Skill: Level (1-20) Herald of Blood\n{variant:28}Grants Skill: Level (1-20) Withering Presence\n{variant:29}Grants Skill: Level (1-20) Ravenous Swarm\n{variant:30}Grants Skill: Level (1-20) Combat Frenzy\n{variant:31}Grants Skill: Level (1-20) Trail of Caltrops\n{variant:32}Grants Skill: Level (1-20) Convalescence\n{variant:33}Grants Skill: Level (1-20) Iron Ward\n{variant:34}Grants Skill: Level (1-20) Siphon Elements\n{variant:35}Grants Skill: Level (1-20) Briarpatch\n{variant:36}Grants Skill: Level (1-20) Savage Fury\n{variant:37}Grants Skill: Level (1-20) Barkskin", implicitModTypes = { { }, }, @@ -122,6 +123,7 @@ itemBases["Lament Amulet"] = { itemBases["Portent Amulet"] = { type = "Amulet", tags = { amulet = true, default = true, }, + grantedSkillsHaveNoReservation = true, variantList = { "Wolf Pack", "Blink", "Elemental Invocation", "Elemental Conflux", "Sacrifice", "Mirage Archer", "Feral Invocation", }, implicit = "-1 Suffix Modifier allowed\n{variant:1}Grants Skill: Level (1-20) Wolf Pack\n{variant:2}Grants Skill: Level (1-20) Blink\n{variant:3}Grants Skill: Level (1-20) Elemental Invocation\n{variant:4}Grants Skill: Level (1-20) Elemental Conflux\n{variant:5}Grants Skill: Level (1-20) Sacrifice\n{variant:6}Grants Skill: Level (1-20) Mirage Archer\n{variant:7}Grants Skill: Level (1-20) Feral Invocation", implicitModTypes = { { }, }, @@ -130,7 +132,7 @@ itemBases["Portent Amulet"] = { itemBases["Absent Amulet"] = { type = "Amulet", tags = { amulet = true, default = true, }, - grantedSkillsReserveNoSpirit = true, + grantedSkillsHaveNoReservation = true, variantList = { "Cast on Elemental Ailment", "Cast on Critical", "Cast on Dodge", "Rhoa Mount", "Archmage", "Trinity", "Eternal Rage", }, implicit = "-1 Prefix Modifier allowed\n-1 Suffix Modifier allowed\n{variant:1}Grants Skill: Level (1-20) Cast on Elemental Ailment\n{variant:2}Grants Skill: Level (1-20) Cast on Critical\n{variant:3}Grants Skill: Level (1-20) Cast on Dodge\n{variant:4}Grants Skill: Level (1-20) Rhoa Mount\n{variant:5}Grants Skill: Level (1-20) Archmage\n{variant:6}Grants Skill: Level (1-20) Trinity\n{variant:7}Grants Skill: Level (1-20) Eternal Rage", implicitModTypes = { { }, { }, }, diff --git a/src/Export/Scripts/bases.lua b/src/Export/Scripts/bases.lua index f2d4145c30..468ad060dc 100644 --- a/src/Export/Scripts/bases.lua +++ b/src/Export/Scripts/bases.lua @@ -140,8 +140,9 @@ directiveTable.base = function(state, args, out) out:write(tag, ' = true, ') end out:write('},\n') - if baseTypeId == "Metadata/Items/Amulets/FourAmuletB1c" then - out:write('\tgrantedSkillsReserveNoSpirit = true,\n') + local inherentSkillType = dat("ItemInherentSkills"):GetRow("BaseItemType", baseItemType) + if inherentSkillType and inherentSkillType.NoReservation then + out:write('\tgrantedSkillsHaveNoReservation = true,\n') end local implicitLines = { } local implicitModTypes = { } @@ -159,7 +160,6 @@ directiveTable.base = function(state, args, out) if state.type == "Belt" then table.insert(implicitLines, "Has (1-3) Charm Slots") end - local inherentSkillType = dat("ItemInherentSkills"):GetRow("BaseItemType", baseItemType) if inherentSkillType then if #inherentSkillType.Skill > 1 then for index, skill in ipairs(inherentSkillType.Skill) do diff --git a/src/Export/spec.lua b/src/Export/spec.lua index faaef357a8..79069ef89b 100644 --- a/src/Export/spec.lua +++ b/src/Export/spec.lua @@ -31728,10 +31728,10 @@ return { }, [3]={ list=false, - name="Mainhand", + name="NoReservation", refTo="", type="Bool", - width=50 + width=80 } }, itemisedcorpse={ diff --git a/src/Modules/CalcActiveSkill.lua b/src/Modules/CalcActiveSkill.lua index 05aab91e53..49d8ca6c40 100644 --- a/src/Modules/CalcActiveSkill.lua +++ b/src/Modules/CalcActiveSkill.lua @@ -795,6 +795,12 @@ function calcs.buildActiveSkillModList(env, activeSkill) grantedEffectLevel[k] = v end end + if activeEffect.srcInstance and activeEffect.srcInstance.noReservation then + for _, resource in ipairs({ "mana", "life", "spirit" }) do + grantedEffectLevel[resource.."ReservationFlat"] = 0 + grantedEffectLevel[resource.."ReservationPercent"] = 0 + end + end activeEffect.grantedEffectLevel = grantedEffectLevel -- Add extra modifiers from granted effect level diff --git a/src/Modules/CalcDefence.lua b/src/Modules/CalcDefence.lua index 35f471e818..c21c9e199c 100644 --- a/src/Modules/CalcDefence.lua +++ b/src/Modules/CalcDefence.lua @@ -252,10 +252,6 @@ function calcs.doActorLifeManaSpiritReservation(actor) pool.Life.basePercent = pool.Life.basePercent + pool.Spirit.basePercent * spiritToLifeReservation pool.Spirit.basePercent = 0 end - if activeSkill.activeEffect.srcInstance and activeSkill.activeEffect.srcInstance.noReservation then - for _, values in pairs(pool) do values.baseFlat, values.basePercent = 0, 0 end - pool.Spirit.baseFlat = skillModList:Sum("BASE", skillCfg, "ExtraSpirit") - end for name, values in pairs(pool) do values.more = skillModList:More(skillCfg, name.."Reserved", "Reserved") values.inc = skillModList:Sum("INC", skillCfg, name.."Reserved", "Reserved") From a21033cdc29dad7273aea3d8b85616b70b799111 Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Fri, 10 Jul 2026 22:24:50 +1000 Subject: [PATCH 3/3] Move into inherentSkillType block --- src/Export/Scripts/bases.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Export/Scripts/bases.lua b/src/Export/Scripts/bases.lua index 913ca03056..cd55bd6a82 100644 --- a/src/Export/Scripts/bases.lua +++ b/src/Export/Scripts/bases.lua @@ -170,10 +170,10 @@ directiveTable.base = function(state, args, out) end end local inherentSkillType = dat("ItemInherentSkills"):GetRow("BaseItemType", baseItemType) - if inherentSkillType and inherentSkillType.NoReservation then - out:write('\tgrantedSkillsHaveNoReservation = true,\n') - end if inherentSkillType then + if inherentSkillType.NoReservation then + out:write('\tgrantedSkillsHaveNoReservation = true,\n') + end local hasVariants = #inherentSkillType.Skill > 1 for index, skill in ipairs(inherentSkillType.Skill) do local skillGem = dat("SkillGems"):GetRow("BaseItemType", skill.BaseItemType)