Skip to content

Commit 931d197

Browse files
ltogniolliLocalIdentity
andauthored
Add support for Loyalty/damage taken from Companion's Life before you (#2159)
* Add support for damage taken from Companion's Life before you Implements the "% of Damage from Hits is taken from your (Damageable) Companion's Life before you" mechanic as a new "companion" ally pool in the existing alliesTakenBeforeYou framework, covering: - Tree nodes, Starkonja's Head, and the Bonded rune (hit redirect) - Forgotten Warden (Deflected Hits variant, scaled by Deflect chance) - Loyalty and Romira's Requital support gems via a shared statMap entry (Romira's recoup is left unsupported for a follow-up) Total companion life is summed automatically from all active damageable companion skills: the main-skill minion init in CalcPerform is extracted into initMinionModDB so non-main companions can be initialised the same way, with a "Total Companion Life" config field as a manual override. Also drops the stale ifSkill="Raise Spectre" gate from the Total Spectre Life config field so it is reachable in PoE2, and regenerates the four mis-parsed companion entries in ModCache. * Fix ModParser + duplicated function All the companion mods have damageable in the name so no need to exclude it Added Override to the config to its clearer to users without needing to hover Made the minion mod loop into a function as its used multiple times * Fixes Now auto calcs for spectres too Changes the mod name so it's capilatised as it now appears in a breakdown list Fixes the handling of Loyalty as it previously wasn't stacking when using multiple companions --------- Co-authored-by: LocalIdentity <localidentity2@gmail.com>
1 parent a4699d9 commit 931d197

9 files changed

Lines changed: 385 additions & 117 deletions

File tree

spec/System/TestDefence_spec.lua

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,4 +607,139 @@ describe("TestDefence", function()
607607
assertClose(block.EffectiveBlockChance, 10)
608608
assert.is_true(block.TotalEHP > base.TotalEHP)
609609
end)
610+
611+
describe("damage taken from spectres' life before you", function()
612+
it("sums spectre life automatically", function()
613+
build.skillsTab:PasteSocketGroup("Spectre: Lightless Abomination 20/0 1")
614+
build.configTab:BuildModList()
615+
build.configTab.modList:NewMod("TakenFromSpectresBeforeYou", "BASE", 15, "Test")
616+
build.calcsTab:BuildOutput()
617+
618+
local output = build.calcsTab.mainOutput
619+
assert.are.equals(15, output.SpectreAllyDamageMitigation)
620+
local spectreLife = build.calcsTab.mainEnv.player.spectreLifeList[1].life
621+
assert.are.equals(spectreLife, output.TotalSpectreLife)
622+
end)
623+
624+
it("uses configured spectre life as an override", function()
625+
build.skillsTab:PasteSocketGroup("Spectre: Lightless Abomination 20/0 1")
626+
build.configTab.input.TotalSpectreLife = 1000
627+
build.configTab:BuildModList()
628+
build.configTab.modList:NewMod("TakenFromSpectresBeforeYou", "BASE", 15, "Test")
629+
build.calcsTab:BuildOutput()
630+
631+
assert.are.equals(1000, build.calcsTab.mainOutput.TotalSpectreLife)
632+
end)
633+
end)
634+
635+
describe("damage taken from companion's life before you", function()
636+
it("redirects damage to the configured companion life pool", function()
637+
build.configTab.input.enemyIsBoss = "None"
638+
build.configTab.input.customMods = ""
639+
pob1and2Compat()
640+
local baseMaxHit = build.calcsTab.calcsOutput.PhysicalMaximumHitTaken
641+
642+
build.configTab.input.customMods = "15% of Damage from Hits is taken from your Damageable Companion's Life before you"
643+
build.configTab.input.TotalCompanionLife = 1000
644+
pob1and2Compat()
645+
646+
local output = build.calcsTab.calcsOutput
647+
assert.are.equals(15, output.CompanionAllyDamageMitigation)
648+
assert.are.equals(1000, output.TotalCompanionLife)
649+
assert.is_true(output.PhysicalMaximumHitTaken > baseMaxHit)
650+
651+
local totalTaken = takenHitFromTypeMaxHit("Physical")
652+
local pools = poolsRemainingAfterTypeMaxHit("Physical")
653+
local drained = 1000 - pools.AlliesTakenBeforeYou["companion"].remaining
654+
assert.are.near(totalTaken * 0.15, drained, 1)
655+
end)
656+
657+
it("stacks redirect sources additively", function()
658+
build.configTab.input.enemyIsBoss = "None"
659+
build.configTab.input.customMods = "\z
660+
5% of Damage from Hits is taken from your Damageable Companion's Life before you\n\z
661+
15% of Damage from Hits is taken from your Damageable Companion's Life before you\n\z
662+
"
663+
build.configTab.input.TotalCompanionLife = 100
664+
pob1and2Compat()
665+
666+
assert.are.equals(20, build.calcsTab.calcsOutput.CompanionAllyDamageMitigation)
667+
end)
668+
669+
it("scales the deflected-hits redirect by deflect chance", function()
670+
build.configTab.input.enemyIsBoss = "None"
671+
build.configTab.input.customMods = "\z
672+
10% of Damage from Deflected Hits is taken from Damageable Companion's Life before you\n\z
673+
+1000 to Deflection Rating\n\z
674+
"
675+
build.configTab.input.TotalCompanionLife = 500
676+
pob1and2Compat()
677+
678+
local output = build.calcsTab.calcsOutput
679+
assert.is_true(output.DeflectChance > 0)
680+
assert.are.near(10 * output.DeflectChance / 100, output.CompanionAllyDamageMitigation, 0.001)
681+
end)
682+
683+
it("sums companion life automatically and applies Loyalty's redirect and life penalty", function()
684+
build.skillsTab:PasteSocketGroup("Companion: Lightless Abomination 20/0 1")
685+
runCallback("OnFrame")
686+
local unsupportedLife = build.calcsTab.calcsEnv.minion.output.Life
687+
688+
newBuild()
689+
build.skillsTab:PasteSocketGroup("Companion: Lightless Abomination 20/0 1\nLoyalty 1/0 1")
690+
runCallback("OnFrame")
691+
692+
local output = build.calcsTab.calcsOutput
693+
local companionLife = build.calcsTab.calcsEnv.minion.output.Life
694+
assert.are.equals(10, output.CompanionAllyDamageMitigation)
695+
assert.are.equals(companionLife, output.TotalCompanionLife)
696+
assert.are.near(unsupportedLife * 0.7, companionLife, 1)
697+
end)
698+
699+
it("sums companion life when the companion is not the main skill", function()
700+
build.skillsTab:PasteSocketGroup("Companion: Lightless Abomination 20/0 1\nLoyalty 1/0 1")
701+
runCallback("OnFrame")
702+
local mainSkillCompanionLife = build.calcsTab.calcsOutput.TotalCompanionLife
703+
704+
newBuild()
705+
build.skillsTab:PasteSocketGroup("Quarterstaff Strike 20/0 1")
706+
build.skillsTab:PasteSocketGroup("Companion: Lightless Abomination 20/0 1\nLoyalty 1/0 1")
707+
build.mainSocketGroup = 1
708+
runCallback("OnFrame")
709+
710+
local output = build.calcsTab.calcsOutput
711+
assert.are.equals(10, output.CompanionAllyDamageMitigation)
712+
assert.are.near(mainSkillCompanionLife, output.TotalCompanionLife, 1)
713+
end)
714+
715+
it("stacks Loyalty's redirect from multiple companion skills", function()
716+
build.skillsTab:PasteSocketGroup("Companion: Lightless Abomination 20/0 1\nLoyalty 1/0 1")
717+
build.skillsTab:PasteSocketGroup("Companion: Lightless Moray 20/0 1\nLoyalty 1/0 1")
718+
runCallback("OnFrame")
719+
720+
assert.are.equals(20, build.calcsTab.calcsOutput.CompanionAllyDamageMitigation)
721+
end)
722+
723+
it("does not count global companion redirect once per companion skill", function()
724+
build.configTab.input.customMods = "5% of Damage from Hits is taken from your Damageable Companion's Life before you"
725+
build.skillsTab:PasteSocketGroup("Companion: Lightless Abomination 20/0 1")
726+
build.skillsTab:PasteSocketGroup("Companion: Lightless Abomination 20/0 1")
727+
pob1and2Compat()
728+
729+
assert.are.equals(5, build.calcsTab.calcsOutput.CompanionAllyDamageMitigation)
730+
end)
731+
732+
it("has no effect with no companions and no config override", function()
733+
build.configTab.input.enemyIsBoss = "None"
734+
build.configTab.input.customMods = ""
735+
pob1and2Compat()
736+
local baseMaxHit = build.calcsTab.calcsOutput.PhysicalMaximumHitTaken
737+
738+
build.configTab.input.customMods = "15% of Damage from Hits is taken from your Damageable Companion's Life before you"
739+
pob1and2Compat()
740+
741+
assert.are.equals(0, build.calcsTab.calcsOutput.TotalCompanionLife)
742+
assert.are.equals(baseMaxHit, build.calcsTab.calcsOutput.PhysicalMaximumHitTaken)
743+
end)
744+
end)
610745
end)

src/Data/ModCache.lua

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1541,6 +1541,7 @@ c["13% increased Skill Speed"]={{[1]={flags=0,keywordFlags=0,name="Speed",type="
15411541
c["13% increased Spell damage for each 200 total Mana you have Spent Recently"]={{[1]={[1]={div=200,type="Multiplier",var="ManaSpentRecently"},flags=2,keywordFlags=0,name="Damage",type="INC",value=13}},nil}
15421542
c["13% increased maximum Life"]={{[1]={flags=0,keywordFlags=0,name="Life",type="INC",value=13}},nil}
15431543
c["13% increased maximum Mana"]={{[1]={flags=0,keywordFlags=0,name="Mana",type="INC",value=13}},nil}
1544+
c["13% of Damage from Deflected Hits is taken from Damageable Companion's Life before you"]={{[1]={flags=0,keywordFlags=0,name="TakenFromCompanionBeforeYouFromDeflected",type="BASE",value=13}},nil}
15441545
c["13% reduced Attack Speed"]={{[1]={flags=1,keywordFlags=0,name="Speed",type="INC",value=-13}},nil}
15451546
c["13% reduced Attack and Cast Speed"]={{[1]={flags=0,keywordFlags=0,name="Speed",type="INC",value=-13}},nil}
15461547
c["13% reduced Charges per use"]={{[1]={flags=0,keywordFlags=0,name="FlaskChargesUsed",type="INC",value=-13}},nil}
@@ -1739,8 +1740,8 @@ c["15% less maximum Mana"]={{[1]={flags=0,keywordFlags=0,name="Mana",type="MORE"
17391740
c["15% more Damage against Enemies affected by Blood Boils"]={{[1]={flags=0,keywordFlags=0,name="Damage",type="MORE",value=15}}," against Enemies affected by Blood Boils "}
17401741
c["15% more Damage against Enemies affected by Blood Boils Grants Skill: Blood Boil"]={{[1]={[1]={includeTransfigured=true,skillName="Blood Boil",type="SkillName"},flags=0,keywordFlags=0,name="Damage",type="MORE",value=15}}," against Enemies affected by Blood Boils Grants Skill:"}
17411742
c["15% more Maximum Life"]={{[1]={flags=0,keywordFlags=0,name="Life",type="MORE",value=15}},nil}
1742-
c["15% of Damage from Deflected Hits is taken from Damageable Companion's Life before you"]={{[1]={flags=0,keywordFlags=0,name="Damage",type="BASE",value=15}}," from Deflected Hits is taken from Damageable Companion's Life before you "}
1743-
c["15% of Damage from Hits is taken from your Damageable Companion's Life before you"]={{[1]={flags=0,keywordFlags=0,name="Damage",type="BASE",value=15}}," from Hits is taken from your Damageable Companion's Life before you "}
1743+
c["15% of Damage from Deflected Hits is taken from Damageable Companion's Life before you"]={{[1]={flags=0,keywordFlags=0,name="TakenFromCompanionBeforeYouFromDeflected",type="BASE",value=15}},nil}
1744+
c["15% of Damage from Hits is taken from your Damageable Companion's Life before you"]={{[1]={flags=0,keywordFlags=0,name="TakenFromCompanionBeforeYou",type="BASE",value=15}},nil}
17441745
c["15% of Damage is taken from Mana before Life"]={{[1]={flags=0,keywordFlags=0,name="DamageTakenFromManaBeforeLife",type="BASE",value=15}},nil}
17451746
c["15% of Damage taken Recouped as Life"]={{[1]={flags=0,keywordFlags=0,name="LifeRecoup",type="BASE",value=15}},nil}
17461747
c["15% of Damage taken from Deflected Hits Recouped as Life"]={{[1]={flags=0,keywordFlags=0,name="DamageTaken",type="BASE",value=15}}," from Deflected Hits Recouped as Life "}
@@ -2150,7 +2151,7 @@ c["20% more Damage against Heavy Stunned Enemies with Maces"]={{[1]={[1]={actor=
21502151
c["20% more Life Cost of Skills"]={{[1]={flags=0,keywordFlags=0,name="LifeCost",type="MORE",value=20}},nil}
21512152
c["20% more Stun Buildup with Critical Hits"]={{[1]={[1]={type="Condition",var="CriticalStrike"},flags=0,keywordFlags=0,name="EnemyHeavyStunBuildup",type="MORE",value=20}},nil}
21522153
c["20% of Cold Damage taken as Fire Damage"]={{[1]={flags=0,keywordFlags=0,name="ColdDamageTakenAsFire",type="BASE",value=20}},nil}
2153-
c["20% of Damage from Hits is taken from your nearest Totem's Life before you"]={{[1]={[1]={type="Condition",var="HaveTotem"},flags=0,keywordFlags=0,name="takenFromTotemsBeforeYou",type="BASE",value=20}},nil}
2154+
c["20% of Damage from Hits is taken from your nearest Totem's Life before you"]={{[1]={[1]={type="Condition",var="HaveTotem"},flags=0,keywordFlags=0,name="TakenFromTotemsBeforeYou",type="BASE",value=20}},nil}
21542155
c["20% of Damage is taken from Mana before Life"]={{[1]={flags=0,keywordFlags=0,name="DamageTakenFromManaBeforeLife",type="BASE",value=20}},nil}
21552156
c["20% of Damage taken Recouped as Life"]={{[1]={flags=0,keywordFlags=0,name="LifeRecoup",type="BASE",value=20}},nil}
21562157
c["20% of Damage taken Recouped as Mana"]={{[1]={flags=0,keywordFlags=0,name="ManaRecoup",type="BASE",value=20}},nil}
@@ -3082,8 +3083,7 @@ c["5% increased Stun Threshold"]={{[1]={flags=0,keywordFlags=0,name="StunThresho
30823083
c["5% increased Stun Threshold per 25 Tribute"]={{[1]={[1]={actor="parent",div=25,stat="Tribute",type="PerStat"},flags=0,keywordFlags=0,name="StunThreshold",type="INC",value=5}},nil}
30833084
c["5% increased effect of Archon Buffs on you"]={{[1]={flags=0,keywordFlags=0,name="LocalEffect",type="INC",value=5}}," of Archon Buffs on you "}
30843085
c["5% increased total Power counted by Warcries"]={{[1]={flags=0,keywordFlags=0,name="WarcryPower",type="INC",value=5}},nil}
3085-
c["5% of Damage from Hits is taken from your Damageable Companion's Life before you"]={{[1]={flags=0,keywordFlags=0,name="Damage",type="BASE",value=5}}," from Hits is taken from your Damageable Companion's Life before you "}
3086-
c["5% of Damage from Hits is taken from your Damageable Companion's Life before you 20% increased Armour, Evasion and Energy Shield while your Companion is in your Presence"]={{[1]={[1]={type="Condition",var="CompanionInPresence"},flags=0,keywordFlags=0,name="Damage",type="BASE",value=5}}," from Hits is taken from your Damageable Companion's Life before you 20% increased Armour, Evasion and Energy Shield "}
3086+
c["5% of Damage from Hits is taken from your Damageable Companion's Life before you"]={{[1]={flags=0,keywordFlags=0,name="TakenFromCompanionBeforeYou",type="BASE",value=5}},nil}
30873087
c["5% of Damage taken Recouped as Life"]={{[1]={flags=0,keywordFlags=0,name="LifeRecoup",type="BASE",value=5}},nil}
30883088
c["5% of Damage taken bypasses Energy Shield"]={{[1]={flags=0,keywordFlags=0,name="PhysicalEnergyShieldBypass",type="BASE",value=5},[2]={flags=0,keywordFlags=0,name="LightningEnergyShieldBypass",type="BASE",value=5},[3]={flags=0,keywordFlags=0,name="ColdEnergyShieldBypass",type="BASE",value=5},[4]={flags=0,keywordFlags=0,name="FireEnergyShieldBypass",type="BASE",value=5},[5]={flags=0,keywordFlags=0,name="ChaosEnergyShieldBypass",type="BASE",value=5}},nil}
30893089
c["5% of Maximum Life Converted to Energy Shield"]={{[1]={flags=0,keywordFlags=0,name="LifeConvertToEnergyShield",type="BASE",value=5}},nil}

src/Data/SkillStatMap.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2527,6 +2527,9 @@ return {
25272527
["companions_are_gigantic"] = {
25282528
mod("MinionModifier", "LIST", { mod = flag("Gigantic") }),
25292529
},
2530+
["companion_takes_%_damage_before_you_from_support"] = {
2531+
mod("TakenFromCompanionBeforeYou", "BASE", nil, 0, 0, { type = "GlobalEffect", effectType = "Buff", unscalable = true }),
2532+
},
25302533
["minion_damage_+%_final_per_different_elemental_ailment_on_target"] = {
25312534
mod("MinionModifier", "LIST", { mod = mod("Damage", "MORE", nil, 0, 0, { type = "ActorCondition", actor = "enemy", var = "Electrocuted" }) }),
25322535
mod("MinionModifier", "LIST", { mod = mod("Damage", "MORE", nil, 0, 0, { type = "ActorCondition", actor = "enemy", var = "Frozen" }) }),

src/Modules/CalcActiveSkill.lua

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,15 @@ function calcs.buildActiveSkillModList(env, activeSkill)
10281028
if tag.type == "GlobalEffect" then
10291029
effectType = tag.effectType
10301030
effectName = tag.effectName or activeGrantedEffect.name
1031+
if activeSkill.minion and activeSkill.minion.minionData then
1032+
if effectName:find("{0}", 1, true) then
1033+
effectName = effectName:gsub("{0}", activeSkill.minion.minionData.name)
1034+
elseif activeGrantedEffect.minionList and effectName:match("^Companion") then
1035+
effectName = "Companion: "..activeSkill.minion.minionData.name
1036+
elseif activeGrantedEffect.minionList and effectName:match("^Spectre") then
1037+
effectName = "Spectre: "..activeSkill.minion.minionData.name
1038+
end
1039+
end
10311040
effectTag = tag
10321041
break
10331042
end

0 commit comments

Comments
 (0)