From 5dd2c3d4cc800a421e2f184da54fa807a183f147 Mon Sep 17 00:00:00 2001 From: jokker23 Date: Sun, 5 Jul 2026 11:55:48 -0600 Subject: [PATCH] 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)