From ad95d2d276e5954c2fdb3ec8120be9e0c4986a5a Mon Sep 17 00:00:00 2001 From: cupkax Date: Fri, 26 Jun 2026 14:02:30 +1000 Subject: [PATCH 1/4] Add support for Atziri's Communion support gem Supports Persistent Skills by reserving Life instead of Spirit. --- src/Data/Gems.lua | 19 +++++++++++++++++++ src/Data/SkillStatMap.lua | 4 ++++ src/Data/Skills/sup_int.lua | 32 ++++++++++++++++++++++++++++++++ src/Export/Skills/sup_int.txt | 5 +++++ src/Modules/CalcDefence.lua | 30 +++++++++++++++++++++++++++++- 5 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/Data/Gems.lua b/src/Data/Gems.lua index dce7d86ec7..9e4ce6b68f 100644 --- a/src/Data/Gems.lua +++ b/src/Data/Gems.lua @@ -17432,6 +17432,25 @@ return { Tier = 0, naturalMaxLevel = 1, }, + ["Metadata/Items/Gems/SkillGemAtzirisCommunionSupport"] = { + name = "Atziri's Communion", + gameId = "Metadata/Items/Gem/SupportGemAtzirisCommunion", + variantId = "AtzirisCommunionSupport", + grantedEffectId = "SupportAtzirisCommunionPlayer", + tags = { + support = true, + lineage = true, + persistent = true, + }, + gemType = "Support", + gemFamily = "Atziri's Communion", + tagString = "Lineage, Persistent", + reqStr = 0, + reqDex = 0, + reqInt = 100, + Tier = 0, + naturalMaxLevel = 1, + }, ["Metadata/Items/Gems/SkillGemAtzirisImpatienceSupport"] = { name = "Atziri's Impatience", gameId = "Metadata/Items/Gem/SupportGemAtzirisImpatience", diff --git a/src/Data/SkillStatMap.lua b/src/Data/SkillStatMap.lua index f25bb4f0f7..33fafaece8 100644 --- a/src/Data/SkillStatMap.lua +++ b/src/Data/SkillStatMap.lua @@ -179,6 +179,10 @@ return { ["base_skill_reserve_life_instead_of_mana"] = { flag("BloodMagicReserved"), }, +["skill_reserves_X_life_permyriad_per_spirit_instead_of_spirit"] = { + mod("LifeReservePercentPerSpirit", "BASE", nil), + div = 100, +}, ["base_skill_cost_life_instead_of_mana"] = { flag("CostLifeInsteadOfMana"), }, diff --git a/src/Data/Skills/sup_int.lua b/src/Data/Skills/sup_int.lua index cdaf7115e4..cec9e52d38 100644 --- a/src/Data/Skills/sup_int.lua +++ b/src/Data/Skills/sup_int.lua @@ -654,6 +654,38 @@ skills["SupportAstralProjectionPlayer"] = { }, } } +skills["SupportAtzirisCommunionPlayer"] = { + name = "Atziri's Communion", + description = "Supports Persistent Skills, making them Reserve Life instead of Spirit. Cannot Support Skills which create Minions.", + color = 3, + support = true, + requireSkillTypes = { SkillType.Persistent, }, + addSkillTypes = { }, + excludeSkillTypes = { SkillType.CreatesMinion, }, + gemFamily = { "AtziriCommunionLineage",}, + isLineage = true, + flavourText = {"The Red Communion was meant to transcend the limits", "of the soul, to transfigure the flesh, to bestow immortality.", "It accomplished all of these things... most horribly.", }, + levels = { + [1] = { levelRequirement = 0, }, + }, + statSets = { + [1] = { + label = "Atziri's Communion", + incrementalEffectiveness = 0.054999999701977, + statDescriptionScope = "gem_stat_descriptions", + baseFlags = { + }, + constantStats = { + { "skill_reserves_X_life_permyriad_per_spirit_instead_of_spirit", 66 }, + }, + stats = { + }, + levels = { + [1] = { actorLevel = 1, }, + }, + }, + } +} skills["SupportAtzirisAllurePlayer"] = { name = "Atziri's Allure", description = "Supports Curse Spells you cast yourself, causing those Curses to ignore the usual Curse Limit, but be reflected back to you when inflicted.", diff --git a/src/Export/Skills/sup_int.txt b/src/Export/Skills/sup_int.txt index e2f32cf0f2..1bd5eeda12 100644 --- a/src/Export/Skills/sup_int.txt +++ b/src/Export/Skills/sup_int.txt @@ -107,6 +107,11 @@ statMap = { #mods #skillEnd +#skill SupportAtzirisCommunionPlayer +#set SupportAtzirisCommunionPlayer +#mods +#skillEnd + #skill SupportAtzirisAllurePlayer #set SupportAtzirisAllurePlayer statMap = { diff --git a/src/Modules/CalcDefence.lua b/src/Modules/CalcDefence.lua index a96b386bdd..04508cf6f3 100644 --- a/src/Modules/CalcDefence.lua +++ b/src/Modules/CalcDefence.lua @@ -235,6 +235,15 @@ function calcs.doActorLifeManaSpiritReservation(actor) activeSkill.skillData["LifeReservationPercentForced"] = activeSkill.skillData["ManaReservationPercentForced"] activeSkill.skillData["ManaReservationPercentForced"] = nil end + do + local lifePerSpirit = skillModList:Sum("BASE", skillCfg, "LifeReservePercentPerSpirit") + if lifePerSpirit > 0 then + pool.Life.basePercent = pool.Life.basePercent + pool.Spirit.baseFlat * lifePerSpirit + pool.Spirit.baseFlat = 0 + pool.Life.basePercent = pool.Life.basePercent + pool.Spirit.basePercent * lifePerSpirit + pool.Spirit.basePercent = 0 + end + 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") @@ -280,7 +289,26 @@ function calcs.doActorLifeManaSpiritReservation(actor) -- Extra reservation of blasphemy needs to be separated from the reservation caused by curses local blasphemyFlat = activeSkill.skillData["blasphemyReservationFlat" .. name] local blasphemyEffectiveFlat = m_max(round(blasphemyFlat * mult * (100 + values.inc) / 100 * values.more / (1 + values.efficiency / 100) / values.efficiencyMore, 0), 0) - values.reservedFlat = values.reservedFlat + blasphemyEffectiveFlat * instances + local lifePerSpirit = skillModList:Sum("BASE", skillCfg, "LifeReservePercentPerSpirit") + if name == "Spirit" and lifePerSpirit > 0 then + local lifeBasePercent = blasphemyFlat * instances * lifePerSpirit + local lifeEffectivePercent = m_max(round(lifeBasePercent * mult * (100 + values.inc) / 100 * values.more / (1 + values.efficiency / 100) / values.efficiencyMore, 2), 0) + actor["reserved_LifePercent"] = actor["reserved_LifePercent"] + lifeEffectivePercent + if breakdown then + t_insert(breakdown["LifeReserved"].reservations, { + skillName = activeSkill.activeEffect.grantedEffect.name, + base = lifeBasePercent .. "%", + mult = mult ~= 1 and ("x "..mult), + more = values.more ~= 1 and ("x "..values.more), + inc = values.inc ~= 0 and ("x "..(1 + values.inc / 100)), + efficiency = values.efficiency ~= 0 and ("x " .. round(100 / (100 + values.efficiency), 4)), + efficiencyMore = values.efficiencyMore ~= 1 and ("x "..values.efficiencyMore), + total = lifeEffectivePercent .. "%", + }) + end + else + values.reservedFlat = values.reservedFlat + blasphemyEffectiveFlat * instances + end end -- Blood Sacrament increases reservation per stage channelled if activeSkill.skillCfg.skillName == "Blood Sacrament" and activeSkill.activeStageCount then From a9b1dfac23ae3eb5b7f385c1a31db5b7e1fcb3e0 Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Fri, 26 Jun 2026 17:31:19 +1000 Subject: [PATCH 2/4] Simplify calc and add test --- spec/System/TestSkills_spec.lua | 17 ++++++++++ src/Modules/CalcDefence.lua | 55 ++++++++++----------------------- 2 files changed, 33 insertions(+), 39 deletions(-) diff --git a/spec/System/TestSkills_spec.lua b/spec/System/TestSkills_spec.lua index 9609d08349..c4c8eaf9b8 100644 --- a/spec/System/TestSkills_spec.lua +++ b/spec/System/TestSkills_spec.lua @@ -94,6 +94,23 @@ describe("TestSkills", function() assert.True(build.calcsTab.mainOutput.SpiritReservedPercent > oneCurseReservation) end) + it("applies life reservation efficiency to Atziri's Communion Blasphemy reservation", function() + build.skillsTab:PasteSocketGroup("Blasphemy 20/0 1\nDespair 20/0 1\nAtziri's Communion 1/0 1\n") + runCallback("OnFrame") + + assert.are.equals(0, build.calcsTab.mainOutput.SpiritReserved) + assert.are.equals(0, build.calcsTab.mainOutput.SpiritReservedPercent) + assert.are.equals(26, build.calcsTab.mainOutput.LifeReserved) + assert.are.equals(40, build.calcsTab.mainOutput.LifeReservedPercent) + + build.configTab.input.customMods = "100% increased Life Reservation Efficiency" + build.configTab:BuildModList() + runCallback("OnFrame") + + assert.are.equals(13, build.calcsTab.mainOutput.LifeReserved) + assert.are.equals(20, build.calcsTab.mainOutput.LifeReservedPercent) + end) + it("applies active skill reservation multiplier to linked buff spirit reservation", function() build.skillsTab:PasteSocketGroup("Purity of Fire 20/0 1\nVitality II 1/0 1\n") runCallback("OnFrame") diff --git a/src/Modules/CalcDefence.lua b/src/Modules/CalcDefence.lua index 04508cf6f3..3b20aae196 100644 --- a/src/Modules/CalcDefence.lua +++ b/src/Modules/CalcDefence.lua @@ -225,6 +225,16 @@ function calcs.doActorLifeManaSpiritReservation(actor) pool.Life.baseFlat = skillModList:Sum("BASE", skillCfg, "LifeCostBase") + (activeSkill.activeEffect.grantedEffectLevel.cost.Life or 0) end pool.Life.basePercent = activeSkill.skillData.lifeReservationPercent or activeSkill.activeEffect.grantedEffectLevel.lifeReservationPercent or 0 + if activeSkill.skillTypes[SkillType.IsBlasphemy] and activeSkill.activeEffect.srcInstance.supportEffect and activeSkill.activeEffect.srcInstance.supportEffect.isSupporting then + -- Sadly no better way to get key/val table element count in lua. + local instances = 0 + for _ in pairs(activeSkill.activeEffect.srcInstance.supportEffect.isSupporting) do + instances = instances + 1 + end + for name, values in pairs(pool) do + values.baseFlat = values.baseFlat + (activeSkill.skillData["blasphemyReservationFlat" .. name] or 0) * instances + end + end if skillModList:Flag(skillCfg, "BloodMagicReserved") then pool.Life.baseFlat = pool.Life.baseFlat + pool.Mana.baseFlat pool.Mana.baseFlat = 0 @@ -235,14 +245,12 @@ function calcs.doActorLifeManaSpiritReservation(actor) activeSkill.skillData["LifeReservationPercentForced"] = activeSkill.skillData["ManaReservationPercentForced"] activeSkill.skillData["ManaReservationPercentForced"] = nil end - do - local lifePerSpirit = skillModList:Sum("BASE", skillCfg, "LifeReservePercentPerSpirit") - if lifePerSpirit > 0 then - pool.Life.basePercent = pool.Life.basePercent + pool.Spirit.baseFlat * lifePerSpirit - pool.Spirit.baseFlat = 0 - pool.Life.basePercent = pool.Life.basePercent + pool.Spirit.basePercent * lifePerSpirit - pool.Spirit.basePercent = 0 - end + local spiritToLifeReservation = skillModList:Sum("BASE", skillCfg, "LifeReservePercentPerSpirit") + if spiritToLifeReservation > 0 then + pool.Life.basePercent = pool.Life.basePercent + pool.Spirit.baseFlat * spiritToLifeReservation + pool.Spirit.baseFlat = 0 + pool.Life.basePercent = pool.Life.basePercent + pool.Spirit.basePercent * spiritToLifeReservation + pool.Spirit.basePercent = 0 end for name, values in pairs(pool) do values.more = skillModList:More(skillCfg, name.."Reserved", "Reserved") @@ -278,37 +286,6 @@ function calcs.doActorLifeManaSpiritReservation(actor) values.count = activeSkillCount local minionFreeSpiritCount = skillModList:Sum("BASE", skillCfg, "MinionFreeSpiritCount") values.reservedFlat = values.reservedFlat * m_max(activeSkillCount - minionFreeSpiritCount, 0) - end - if activeSkill.skillTypes[SkillType.IsBlasphemy] and activeSkill.activeEffect.srcInstance.supportEffect and activeSkill.activeEffect.srcInstance.supportEffect.isSupporting and activeSkill.skillData["blasphemyReservationFlat" .. name] then - -- Sadly no better way to get key/val table element count in lua. - local instances = 0 - for _ in pairs(activeSkill.activeEffect.srcInstance.supportEffect.isSupporting) do - instances = instances + 1 - end - - -- Extra reservation of blasphemy needs to be separated from the reservation caused by curses - local blasphemyFlat = activeSkill.skillData["blasphemyReservationFlat" .. name] - local blasphemyEffectiveFlat = m_max(round(blasphemyFlat * mult * (100 + values.inc) / 100 * values.more / (1 + values.efficiency / 100) / values.efficiencyMore, 0), 0) - local lifePerSpirit = skillModList:Sum("BASE", skillCfg, "LifeReservePercentPerSpirit") - if name == "Spirit" and lifePerSpirit > 0 then - local lifeBasePercent = blasphemyFlat * instances * lifePerSpirit - local lifeEffectivePercent = m_max(round(lifeBasePercent * mult * (100 + values.inc) / 100 * values.more / (1 + values.efficiency / 100) / values.efficiencyMore, 2), 0) - actor["reserved_LifePercent"] = actor["reserved_LifePercent"] + lifeEffectivePercent - if breakdown then - t_insert(breakdown["LifeReserved"].reservations, { - skillName = activeSkill.activeEffect.grantedEffect.name, - base = lifeBasePercent .. "%", - mult = mult ~= 1 and ("x "..mult), - more = values.more ~= 1 and ("x "..values.more), - inc = values.inc ~= 0 and ("x "..(1 + values.inc / 100)), - efficiency = values.efficiency ~= 0 and ("x " .. round(100 / (100 + values.efficiency), 4)), - efficiencyMore = values.efficiencyMore ~= 1 and ("x "..values.efficiencyMore), - total = lifeEffectivePercent .. "%", - }) - end - else - values.reservedFlat = values.reservedFlat + blasphemyEffectiveFlat * instances - end end -- Blood Sacrament increases reservation per stage channelled if activeSkill.skillCfg.skillName == "Blood Sacrament" and activeSkill.activeStageCount then From 4e73e7f056d10ebdfcf24d4a82911e1b0e69f733 Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Fri, 26 Jun 2026 17:35:07 +1000 Subject: [PATCH 3/4] Fix ModCache test --- src/Data/ModCache.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Data/ModCache.lua b/src/Data/ModCache.lua index 5390b1ef90..3abe158a2b 100644 --- a/src/Data/ModCache.lua +++ b/src/Data/ModCache.lua @@ -1387,7 +1387,6 @@ c["100% increased Thorns damage if you've consumed an Endurance Charge Recently" c["100% increased amount of Life Leeched"]={{[1]={flags=0,keywordFlags=0,name="MaxLifeLeechRate",type="INC",value=100}},nil} c["100% increased chance to Shock"]={{[1]={flags=0,keywordFlags=0,name="EnemyShockChance",type="INC",value=100}},nil} c["100% increased effect of Socketed Augment Items"]={{[1]={flags=0,keywordFlags=0,name="SocketedAugmentItemEffect",type="INC",value=100}},nil} -c["100% increased effect of Socketed Augment Items This item gains bonuses from Socketed Items as though it was a Body Armour"]={{[1]={flags=0,keywordFlags=0,name="SocketedAugmentItemEffect",type="INC",value=100}}," This item gains bonuses from Socketed Items as though it was a Body Armour "} c["100% increased effect of Socketed Soul Cores"]={{[1]={flags=0,keywordFlags=0,name="SocketedSoulCoreEffect",type="INC",value=100}},nil} c["100% increased maximum Divinity"]={{}," maximum Divinity "} c["100% increased maximum Divinity 20% reduced maximum Divinity per Corrupted Item Equipped"]={{}," maximum Divinity 20% reduced maximum Divinity per Corrupted Item Equipped "} @@ -3448,6 +3447,7 @@ c["75% increased Physical Damage"]={{[1]={flags=0,keywordFlags=0,name="PhysicalD c["75% increased Spirit"]={{[1]={flags=0,keywordFlags=0,name="Spirit",type="INC",value=75}},nil} c["75% increased Thorns damage if you've Blocked Recently"]={{[1]={[1]={type="Condition",var="BlockedRecently"},flags=32,keywordFlags=0,name="Damage",type="INC",value=75}},nil} c["75% increased chance to Shock"]={{[1]={flags=0,keywordFlags=0,name="EnemyShockChance",type="INC",value=75}},nil} +c["75% increased effect of Socketed Augment Items"]={{[1]={flags=0,keywordFlags=0,name="SocketedAugmentItemEffect",type="INC",value=75}},nil} c["75% of Damage Converted to Fire Damage"]={{[1]={flags=0,keywordFlags=0,name="DamageConvertToFire",type="BASE",value=75}},nil} c["75% reduced Amount Recovered"]={{[1]={flags=0,keywordFlags=0,name="FlaskRecovery",type="INC",value=-75}},nil} c["75% reduced Charges per use"]={{[1]={flags=0,keywordFlags=0,name="FlaskChargesUsed",type="INC",value=-75}},nil} From 85680570a8830fc5995cb43caced3b3812c072f3 Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Fri, 26 Jun 2026 17:36:42 +1000 Subject: [PATCH 4/4] Fix alphabetical order --- src/Data/Skills/sup_int.lua | 52 +++++++++++++++++------------------ src/Export/Skills/sup_int.txt | 10 +++---- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/Data/Skills/sup_int.lua b/src/Data/Skills/sup_int.lua index cec9e52d38..a4f1577cc3 100644 --- a/src/Data/Skills/sup_int.lua +++ b/src/Data/Skills/sup_int.lua @@ -654,31 +654,39 @@ skills["SupportAstralProjectionPlayer"] = { }, } } -skills["SupportAtzirisCommunionPlayer"] = { - name = "Atziri's Communion", - description = "Supports Persistent Skills, making them Reserve Life instead of Spirit. Cannot Support Skills which create Minions.", +skills["SupportAtzirisAllurePlayer"] = { + name = "Atziri's Allure", + description = "Supports Curse Spells you cast yourself, causing those Curses to ignore the usual Curse Limit, but be reflected back to you when inflicted.", color = 3, support = true, - requireSkillTypes = { SkillType.Persistent, }, + requireSkillTypes = { SkillType.AppliesCurse, }, addSkillTypes = { }, - excludeSkillTypes = { SkillType.CreatesMinion, }, - gemFamily = { "AtziriCommunionLineage",}, + excludeSkillTypes = { SkillType.UsedByProxy, SkillType.Triggered, SkillType.Persistent, }, + gemFamily = { "AtziriAllureLineage",}, isLineage = true, - flavourText = {"The Red Communion was meant to transcend the limits", "of the soul, to transfigure the flesh, to bestow immortality.", "It accomplished all of these things... most horribly.", }, + flavourText = {"Such was her seductive power, every noble in the court fell", "over themselves to do her bidding. Winning a single glance", "away from her mirror meant more than their lives.", }, + ignoreMinionTypes = true, levels = { [1] = { levelRequirement = 0, }, }, statSets = { [1] = { - label = "Atziri's Communion", + label = "Atziri's Allure", incrementalEffectiveness = 0.054999999701977, statDescriptionScope = "gem_stat_descriptions", + statMap = { + ["support_atziri_curse_effect_+%_final"] = { + mod("CurseEffect", "MORE", nil), + }, + }, baseFlags = { }, constantStats = { - { "skill_reserves_X_life_permyriad_per_spirit_instead_of_spirit", 66 }, + { "support_atziri_curse_effect_+%_final", -20 }, }, stats = { + "curses_reflected_to_self", + "curse_ignores_curse_limit", }, levels = { [1] = { actorLevel = 1, }, @@ -686,39 +694,31 @@ skills["SupportAtzirisCommunionPlayer"] = { }, } } -skills["SupportAtzirisAllurePlayer"] = { - name = "Atziri's Allure", - description = "Supports Curse Spells you cast yourself, causing those Curses to ignore the usual Curse Limit, but be reflected back to you when inflicted.", +skills["SupportAtzirisCommunionPlayer"] = { + name = "Atziri's Communion", + description = "Supports Persistent Skills, making them Reserve Life instead of Spirit. Cannot Support Skills which create Minions.", color = 3, support = true, - requireSkillTypes = { SkillType.AppliesCurse, }, + requireSkillTypes = { SkillType.Persistent, }, addSkillTypes = { }, - excludeSkillTypes = { SkillType.UsedByProxy, SkillType.Triggered, SkillType.Persistent, }, - gemFamily = { "AtziriAllureLineage",}, + excludeSkillTypes = { SkillType.CreatesMinion, }, + gemFamily = { "AtziriCommunionLineage",}, isLineage = true, - flavourText = {"Such was her seductive power, every noble in the court fell", "over themselves to do her bidding. Winning a single glance", "away from her mirror meant more than their lives.", }, - ignoreMinionTypes = true, + flavourText = {"The Red Communion was meant to transcend the limits", "of the soul, to transfigure the flesh, to bestow immortality.", "It accomplished all of these things... most horribly.", }, levels = { [1] = { levelRequirement = 0, }, }, statSets = { [1] = { - label = "Atziri's Allure", + label = "Atziri's Communion", incrementalEffectiveness = 0.054999999701977, statDescriptionScope = "gem_stat_descriptions", - statMap = { - ["support_atziri_curse_effect_+%_final"] = { - mod("CurseEffect", "MORE", nil), - }, - }, baseFlags = { }, constantStats = { - { "support_atziri_curse_effect_+%_final", -20 }, + { "skill_reserves_X_life_permyriad_per_spirit_instead_of_spirit", 66 }, }, stats = { - "curses_reflected_to_self", - "curse_ignores_curse_limit", }, levels = { [1] = { actorLevel = 1, }, diff --git a/src/Export/Skills/sup_int.txt b/src/Export/Skills/sup_int.txt index 1bd5eeda12..019af6bee6 100644 --- a/src/Export/Skills/sup_int.txt +++ b/src/Export/Skills/sup_int.txt @@ -107,11 +107,6 @@ statMap = { #mods #skillEnd -#skill SupportAtzirisCommunionPlayer -#set SupportAtzirisCommunionPlayer -#mods -#skillEnd - #skill SupportAtzirisAllurePlayer #set SupportAtzirisAllurePlayer statMap = { @@ -122,6 +117,11 @@ statMap = { #mods #skillEnd +#skill SupportAtzirisCommunionPlayer +#set SupportAtzirisCommunionPlayer +#mods +#skillEnd + #skill SupportBhatairsVengeancePlayer #set SupportBhatairsVengeancePlayer #mods