diff --git a/spec/System/TestDefence_spec.lua b/spec/System/TestDefence_spec.lua index d0c33555fdb..e0a0060261c 100644 --- a/spec/System/TestDefence_spec.lua +++ b/spec/System/TestDefence_spec.lua @@ -616,6 +616,324 @@ describe("TestDefence", function() end) end) + describe("damage taken from allies' life before you", function() + for _, ally in ipairs({ + { name = "a minion", mod = "takenFromMinionBeforeYou", life = "TotalMinionLife", mitigation = "MinionAllyDamageMitigation", pool = "minion" }, + { name = "spectres", mod = "takenFromSpectresBeforeYou", life = "TotalSpectreLife", mitigation = "SpectreAllyDamageMitigation", pool = "spectres" }, + { name = "totems", mod = "takenFromTotemsBeforeYou", life = "TotalTotemLife", mitigation = "TotemAllyDamageMitigation", pool = "totems" }, + { name = "Sentinel of Radiance", mod = "takenFromRadianceSentinelBeforeYou", life = "TotalRadianceSentinelLife", mitigation = "RadianceSentinelAllyDamageMitigation", pool = "radianceSentinel" }, + { name = "Void Spawns", mod = "takenFromVoidSpawnBeforeYou", life = "TotalVoidSpawnLife", mitigation = "VoidSpawnAllyDamageMitigation", pool = "voidSpawn" }, + }) do + it("redirects damage to "..ally.name, function() + build.configTab:BuildModList() + build.configTab.modList:NewMod(ally.mod, "BASE", 20, "Test") + build.configTab.modList:NewMod(ally.life, "BASE", 1000, "Test") + build.calcsTab:BuildOutput() + + local output = build.calcsTab.mainOutput + assert.are.equals(20, output[ally.mitigation]) + assert.are.equals(1000, output[ally.life]) + local pools = build.calcsTab.calcs.reducePoolsByDamage(nil, { Physical = 100 }, build.calcsTab.mainEnv.player) + assert.are.equals(980, pools.AlliesTakenBeforeYou[ally.pool].remaining) + end) + end + + it("calculates the Companionship minion's life automatically", function() + build.skillsTab:PasteSocketGroup("Fireball 20/0 1") + build.skillsTab:PasteSocketGroup("Raise Zombie 20/0 1\nCompanionship 3/0 1") + build.mainSocketGroup = 1 + build.configTab.input.multiplierSummonedMinion = 2 + build.configTab:BuildModList() + -- Keep the pool visible while Companionship is disabled, so both builds use the same secondary-minion path. + build.configTab.modList:NewMod("takenFromMinionBeforeYou", "BASE", 1, "Test") + build.calcsTab:BuildOutput() + local unsupportedLife = build.calcsTab.mainOutput.TotalMinionLife + + newBuild() + build.skillsTab:PasteSocketGroup("Fireball 20/0 1") + build.skillsTab:PasteSocketGroup("Raise Zombie 20/0 1\nCompanionship 3/0 1") + build.mainSocketGroup = 1 + build.configTab.input.multiplierSummonedMinion = 1 + build.configTab:BuildModList() + build.calcsTab:BuildOutput() + + local output = build.calcsTab.mainOutput + local minionLife = build.calcsTab.mainEnv.player.allyLifeList.TotalMinionLife[1].life + assert.are.equals(15, output.MinionAllyDamageMitigation) + assert.are.equals(minionLife, output.TotalMinionLife) + assert.are.near(unsupportedLife * 1.6, minionLife, 20) + end) + + it("requires exactly one summoned minion for Companionship", function() + build.skillsTab:PasteSocketGroup("Raise Zombie 20/0 1\nCompanionship 3/0 1") + build.configTab.input.multiplierSummonedMinion = 2 + build.configTab:BuildModList() + runCallback("OnFrame") + + assert.are.equals(0, build.calcsTab.calcsOutput.MinionAllyDamageMitigation) + assert.is_nil(build.calcsTab.calcsOutput.TotalMinionLife) + end) + + it("counts a minion without a limit for Companionship", function() + build.skillsTab:PasteSocketGroup("Animate Guardian 20/0 1\nCompanionship 3/0 1") + runCallback("OnFrame") + + assert.are.equals(15, build.calcsTab.calcsOutput.MinionAllyDamageMitigation) + assert.is_true(build.calcsTab.calcsOutput.TotalMinionLife > 0) + end) + + it("counts Companionship's minion outside Combat mode", function() + build.skillsTab:PasteSocketGroup("Animate Guardian 20/0 1\nCompanionship 3/0 1") + build.configTab.input.multiplierSummonedMinion = 0 + build.calcsTab.input.misc_buffMode = "BUFFED" + runCallback("OnFrame") + + assert.are.equals(15, build.calcsTab.calcsOutput.MinionAllyDamageMitigation) + assert.is_true(build.calcsTab.calcsOutput.TotalMinionLife > 0) + end) + + it("does not count invulnerable Minions for Companionship", function() + build.skillsTab:PasteSocketGroup("Animate Guardian 20/0 1\nCompanionship 3/0 1") + build.skillsTab:PasteSocketGroup("Summon Skitterbots 20/0 1") + runCallback("OnFrame") + + assert.are.equals(1, build.calcsTab.calcsEnv.player.modDB:Sum("BASE", nil, "Multiplier:SummonedMinion")) + assert.are.equals(15, build.calcsTab.calcsOutput.MinionAllyDamageMitigation) + assert.is_true(build.calcsTab.calcsOutput.TotalMinionLife > 0) + end) + + it("counts the same Minion type from different skills separately", function() + build.itemsTab:CreateDisplayItemFromRaw("Test Bow\nShort Bow") + build.itemsTab:AddDisplayItem() + build.skillsTab:PasteSocketGroup("Blink Arrow 20/0 1\nCompanionship 3/0 1") + build.skillsTab:PasteSocketGroup("Mirror Arrow 20/0 1") + runCallback("OnFrame") + + assert.are.equals(2, build.calcsTab.calcsEnv.player.modDB:Sum("BASE", nil, "Multiplier:SummonedMinion")) + assert.are.equals(0, build.calcsTab.calcsOutput.MinionAllyDamageMitigation) + end) + + it("counts Minions added by supports for Companionship's condition", function() + build.skillsTab:PasteSocketGroup("Animate Guardian 20/0 1\nCompanionship 3/0 1\nSummon Phantasm 20/0 1") + runCallback("OnFrame") + + assert.are.equals(0, build.calcsTab.calcsOutput.MinionAllyDamageMitigation) + assert.is_nil(build.calcsTab.calcsOutput.TotalMinionLife) + end) + + it("does not count hostile Penance Mark phantasms as your Minions", function() + build.skillsTab:PasteSocketGroup("Animate Guardian 20/0 1\nCompanionship 3/0 1") + build.itemsTab:CreateDisplayItemFromRaw("Test Item\nIron Ring\nGrants level 20 Penance Mark") + build.itemsTab:AddDisplayItem() + runCallback("OnFrame") + + assert.are.equals(15, build.calcsTab.calcsOutput.MinionAllyDamageMitigation) + assert.is_true(build.calcsTab.calcsOutput.TotalMinionLife > 0) + end) + + it("shares Life when Companionship supports an ally with its own redirect", function() + build.skillsTab:PasteSocketGroup("Summon Stone Golem of Safeguarding 20/0 1\nCompanionship 3/0 1") + build.configTab.input.multiplierSummonedMinion = 1 + build.configTab.input.enemyDamageType = "Melee" + build.configTab:BuildModList() + runCallback("OnFrame") + + local output = build.calcsTab.calcsOutput + assert.are.equals(30, output.MinionAllyDamageMitigation) + assert.are.equals(0, output.StoneGolemAllyDamageMitigation) + assert.is_true(output.TotalMinionLife > 0) + assert.is_nil(output.TotalStoneGolemLife) + + build.configTab.input.TotalStoneGolemLife = 1000 + build.configTab:BuildModList() + runCallback("OnFrame") + assert.are.equals(1000, build.calcsTab.calcsOutput.TotalMinionLife) + end) + + it("applies Stone Golem of Safeguarding to melee hits", function() + build.skillsTab:PasteSocketGroup("Summon Stone Golem of Safeguarding 20/0 1") + build.configTab.input.enemyDamageType = "Melee" + build.configTab:BuildModList() + build.configTab.modList:NewMod("ActiveGolemLimit", "BASE", 2, "Test") + build.calcsTab:BuildOutput() + + local output = build.calcsTab.mainOutput + assert.are.equals(15, output.StoneGolemAllyDamageMitigation) + local stoneGolem = build.calcsTab.mainEnv.player.allyLifeList.TotalStoneGolemLife[1] + assert.are.equals(1, stoneGolem.count) + assert.are.equals(stoneGolem.life, output.TotalStoneGolemLife) + local pools = build.calcsTab.calcs.reducePoolsByDamage(nil, { Physical = 100 }, build.calcsTab.mainEnv.player) + assert.are.equals(output.TotalStoneGolemLife - 15, pools.AlliesTakenBeforeYou.stoneGolem.remaining) + + build.configTab.input.enemyDamageType = "Average" + build.configTab:BuildModList() + runCallback("OnFrame") + assert.are.equals(3.75, build.calcsTab.calcsOutput.StoneGolemAllyDamageMitigation) + end) + + it("does not apply Stone Golem of Safeguarding to non-melee hits", function() + build.skillsTab:PasteSocketGroup("Summon Stone Golem of Safeguarding 20/0 1") + build.configTab.input.enemyDamageType = "Spell" + build.configTab:BuildModList() + runCallback("OnFrame") + + assert.are.equals(0, build.calcsTab.calcsOutput.StoneGolemAllyDamageMitigation) + assert.is_nil(build.calcsTab.calcsOutput.TotalStoneGolemLife) + end) + + it("includes support modifiers when the minion is not the main skill", function() + build.skillsTab:PasteSocketGroup("Fireball 20/0 1") + build.skillsTab:PasteSocketGroup("Summon Stone Golem of Safeguarding 20/0 1") + build.mainSocketGroup = 1 + build.configTab.input.enemyDamageType = "Melee" + build.configTab:BuildModList() + runCallback("OnFrame") + local unsupportedLife = build.calcsTab.calcsOutput.TotalStoneGolemLife + + newBuild() + build.skillsTab:PasteSocketGroup("Fireball 20/0 1") + build.skillsTab:PasteSocketGroup("Summon Stone Golem of Safeguarding 20/0 1\nMinion Life 20/0 1") + build.mainSocketGroup = 1 + build.configTab.input.enemyDamageType = "Melee" + build.configTab:BuildModList() + runCallback("OnFrame") + + assert.is_true(build.calcsTab.calcsOutput.TotalStoneGolemLife > unsupportedLife) + end) + + it("keeps the Necromantic Aegis shield between calculator passes", function() + build.spec.allocNodes[45175] = build.spec.nodes[45175] + build.itemsTab:CreateDisplayItemFromRaw("Test Shield\nSplintered Tower Shield") + build.itemsTab:AddDisplayItem() + build.skillsTab:PasteSocketGroup("Animate Guardian 20/0 1") + local env = build.calcsTab.calcs.initEnv(build, "CALCULATOR") + + build.calcsTab.calcs.perform(env) + local blockChance = env.minion.output.BlockChance + assert.is_true(blockChance > 0) + build.calcsTab.calcs.perform(env) + assert.are.equals(blockChance, env.minion.output.BlockChance) + end) + + it("calculates Spectre life automatically and supports an override", function() + build.spectreList = { "Metadata/Monsters/BloodChieftain/MonkeyChiefBloodEnrage" } + build.skillsTab:PasteSocketGroup("Raise Spectre 20/0 1") + build.configTab:BuildModList() + build.configTab.modList:NewMod("takenFromSpectresBeforeYou", "BASE", 15, "Test") + build.calcsTab:BuildOutput() + + local spectre = build.calcsTab.mainEnv.player.allyLifeList.TotalSpectreLife[1] + assert.are.equals(build.calcsTab.mainOutput.ActiveSpectreLimit, spectre.count) + assert.are.equals(build.calcsTab.mainEnv.minion.output.Life * spectre.count, spectre.life) + assert.are.equals(spectre.life, build.calcsTab.mainOutput.TotalSpectreLife) + + build.configTab.input.multiplierSummonedMinion = 1 + build.configTab:BuildModList() + build.configTab.modList:NewMod("takenFromSpectresBeforeYou", "BASE", 15, "Test") + build.calcsTab:BuildOutput() + spectre = build.calcsTab.mainEnv.player.allyLifeList.TotalSpectreLife[1] + assert.are.equals(1, spectre.count) + assert.are.equals(build.calcsTab.mainEnv.minion.output.Life, build.calcsTab.mainOutput.TotalSpectreLife) + + build.configTab.input.TotalSpectreLife = 1000 + build.configTab:BuildModList() + build.configTab.modList:NewMod("takenFromSpectresBeforeYou", "BASE", 15, "Test") + build.calcsTab:BuildOutput() + assert.are.equals(1000, build.calcsTab.mainOutput.TotalSpectreLife) + end) + + it("combines different Spectres without depending on the first one's Life", function() + local chieftain = "Metadata/Monsters/BloodChieftain/MonkeyChiefBloodEnrage" + local meatsack = "Metadata/Monsters/LeagueAzmeri/SpecialCorpses/TankyZombieHigh" + build.spectreList = { chieftain, meatsack } + build.skillsTab:PasteSocketGroup("Raise Spectre 20/0 1") + build.skillsTab:PasteSocketGroup("Raise Spectre 20/0 1\nMinion Life 20/0 1") + build.skillsTab.socketGroupList[1].gemList[1].skillMinion = chieftain + build.skillsTab.socketGroupList[2].gemList[1].skillMinion = meatsack + build.configTab:BuildModList() + build.configTab.modList:NewMod("takenFromSpectresBeforeYou", "BASE", 15, "Test") + build.calcsTab:BuildOutput() + + local spectres = build.calcsTab.mainEnv.player.allyLifeList.TotalSpectreLife + assert.are.equals(2, #spectres) + assert.are.equals(1, spectres[1].count) + assert.are.equals(1, spectres[2].count) + assert.are.equals(spectres[1].life + spectres[2].life, build.calcsTab.mainOutput.TotalSpectreLife) + assert.are_not.equals(spectres[1].life, spectres[2].life) + end) + + it("calculates Totem life automatically", function() + build.skillsTab:PasteSocketGroup("Holy Flame Totem 20/0 1") + build.configTab.input.conditionHaveTotem = true + build.configTab:BuildModList() + build.configTab.modList:NewMod("takenFromTotemsBeforeYou", "BASE", 5, "Test") + build.calcsTab:BuildOutput() + + assert.are.equals(build.calcsTab.mainOutput.TotemLife, build.calcsTab.mainOutput.TotalTotemLife) + assert.are.equals(build.calcsTab.mainOutput.TotemLife, build.calcsTab.mainEnv.player.allyLifeList.TotalTotemLife[1].life) + end) + + it("calculates Vaal Rejuvenation Totem life automatically", function() + build.skillsTab:PasteSocketGroup("Vaal Rejuvenation Totem 20/0 1") + build.configTab:BuildModList() + build.configTab.modList:NewMod("takenFromTotemsBeforeYou", "BASE", 5, "Test") + build.calcsTab:BuildOutput() + + local output = build.calcsTab.mainOutput + local totem = build.calcsTab.mainEnv.player.allyLifeList.TotalVaalRejuvenationTotemLife[1] + assert.are.equals(45, output.VaalRejuvenationTotemAllyDamageMitigation) + assert.are.equals(0, output.TotemAllyDamageMitigation) + assert.are.equals(totem.life, output.TotalVaalRejuvenationTotemLife) + assert.is_nil(output.TotalTotemLife) + end) + + it("calculates Sentinel of Radiance life automatically", function() + build.itemsTab:CreateDisplayItemFromRaw("Test Item\nChainmail Vest\nGrants Level 20 Summon Sentinel of Radiance Skill\n20% of Damage from Hits is taken from your Sentinel of Radiance's Life before you") + build.itemsTab:AddDisplayItem() + runCallback("OnFrame") + + local sentinel = build.calcsTab.calcsEnv.player.allyLifeList.TotalRadianceSentinelLife[1] + assert.are.equals(20, build.calcsTab.calcsOutput.RadianceSentinelAllyDamageMitigation) + assert.are.equals(sentinel.life, build.calcsTab.calcsOutput.TotalRadianceSentinelLife) + end) + + it("calculates total Void Spawn life automatically", function() + build.itemsTab:CreateDisplayItemFromRaw("Servant of Decay\nTorturer Garb\nTrigger Level 20 Summon Void Spawn every 4 seconds\n5% of Damage from Hits is taken from Void Spawns' Life before you per Void Spawn") + build.itemsTab:AddDisplayItem() + runCallback("OnFrame") + + local voidSpawns = build.calcsTab.calcsEnv.player.allyLifeList.TotalVoidSpawnLife[1] + assert.are.equals(4, voidSpawns.count) + assert.are.equals(20, build.calcsTab.calcsOutput.VoidSpawnAllyDamageMitigation) + assert.are.equals(voidSpawns.life, build.calcsTab.calcsOutput.TotalVoidSpawnLife) + end) + + it("handles a full damage redirect without an invalid max hit", function() + build.configTab.input.enemyIsBoss = "None" + build.configTab:BuildModList() + build.configTab.modList:NewMod("takenFromMinionBeforeYou", "BASE", 100, "Test") + build.configTab.modList:NewMod("TotalMinionLife", "BASE", 1000, "Test") + build.calcsTab:BuildOutput() + + assert.are.equals(1060, build.calcsTab.mainOutput.PhysicalMaximumHitTaken) + end) + + it("drains multiple ally pools in a stable order", function() + build.configTab.input.enemyIsBoss = "None" + build.configTab:BuildModList() + build.configTab.modList:NewMod("takenFromMinionBeforeYou", "BASE", 50, "Test") + build.configTab.modList:NewMod("TotalMinionLife", "BASE", 10, "Test") + build.configTab.modList:NewMod("takenFromSpectresBeforeYou", "BASE", 50, "Test") + build.configTab.modList:NewMod("TotalSpectreLife", "BASE", 1000, "Test") + build.calcsTab:BuildOutput() + + local pools = build.calcsTab.calcs.reducePoolsByDamage(nil, { Physical = 100 }, build.calcsTab.mainEnv.player) + assert.are.equals(0, pools.AlliesTakenBeforeYou.minion.remaining) + assert.are.equals(955, pools.AlliesTakenBeforeYou.spectres.remaining) + assert.are.equals(130, build.calcsTab.mainOutput.PhysicalMaximumHitTaken) + end) + end) + local function withinTenPercent(value, otherValue) local ratio = otherValue / value return 0.9 < ratio and ratio < 1.1 diff --git a/src/Data/SkillStatMap.lua b/src/Data/SkillStatMap.lua index 68792c373bd..45856e0efe7 100644 --- a/src/Data/SkillStatMap.lua +++ b/src/Data/SkillStatMap.lua @@ -2081,6 +2081,9 @@ return { value = 100, }, --Golem +["stone_golem_grants_melee_damage_removed_from_stone_golem_before_life_or_es_%"] = { + mod("takenFromStoneGolemBeforeYou", "BASE", nil, 0, 0, { type = "GlobalEffect", effectType = "Buff" }), +}, ["golem_buff_effect_+%"] = { mod("BuffEffect", "INC", nil, 0, 0) }, diff --git a/src/Data/Skills/sup_dex.lua b/src/Data/Skills/sup_dex.lua index 17dd9623b80..39976cdd340 100644 --- a/src/Data/Skills/sup_dex.lua +++ b/src/Data/Skills/sup_dex.lua @@ -1425,7 +1425,7 @@ skills["SupportCompanionship"] = { mod("MinionModifier", "LIST", { mod = mod("Life", "MORE", nil) }, 0, 0, { type = "Condition", var = "OnlyMinion" }), }, ["damage_removed_from_minions_before_life_or_es_%_if_only_one_minion"] = { - mod("takenFromMinionBeforeYou", "BASE", nil, 0, 0, { type = "GlobalEffect", effectType = "Buff", unscalable = true }), + mod("takenFromMinionBeforeYou", "BASE", nil, 0, 0, { type = "GlobalEffect", effectType = "Buff", unscalable = true }, { type = "Condition", var = "OnlyMinion" }), }, }, qualityStats = { diff --git a/src/Export/Skills/sup_dex.txt b/src/Export/Skills/sup_dex.txt index 324de6c0fec..5a025c78783 100644 --- a/src/Export/Skills/sup_dex.txt +++ b/src/Export/Skills/sup_dex.txt @@ -184,7 +184,7 @@ local skills, mod, flag, skill = ... mod("MinionModifier", "LIST", { mod = mod("Life", "MORE", nil) }, 0, 0, { type = "Condition", var = "OnlyMinion" }), }, ["damage_removed_from_minions_before_life_or_es_%_if_only_one_minion"] = { - mod("takenFromMinionBeforeYou", "BASE", nil, 0, 0, { type = "GlobalEffect", effectType = "Buff", unscalable = true }), + mod("takenFromMinionBeforeYou", "BASE", nil, 0, 0, { type = "GlobalEffect", effectType = "Buff", unscalable = true }, { type = "Condition", var = "OnlyMinion" }), }, }, #mods diff --git a/src/Modules/CalcDefence.lua b/src/Modules/CalcDefence.lua index 2362948bc52..72af401ae58 100644 --- a/src/Modules/CalcDefence.lua +++ b/src/Modules/CalcDefence.lua @@ -28,6 +28,31 @@ local dmgTypeList = {"Physical", "Lightning", "Cold", "Fire", "Chaos"} local resistTypeList = { "Fire", "Cold", "Lightning", "Chaos" } +-- Damage is shifted to external allies before Frost Shield, so this is also the order in which pools are drained. +local allyLifePoolList = { + { key = "minion", life = "TotalMinionLife", mitigation = "MinionAllyDamageMitigation", redirect = "takenFromMinionBeforeYou", fallback = "Multiplier:MinionLife", label = "Minion Life" }, + { key = "radianceSentinel", life = "TotalRadianceSentinelLife", mitigation = "RadianceSentinelAllyDamageMitigation", redirect = "takenFromRadianceSentinelBeforeYou", label = "Total Sentinel of Radiance Life" }, + { key = "spectres", life = "TotalSpectreLife", mitigation = "SpectreAllyDamageMitigation", redirect = "takenFromSpectresBeforeYou", label = "Total Spectre Life" }, + { key = "totems", life = "TotalTotemLife", mitigation = "TotemAllyDamageMitigation", redirect = "takenFromTotemsBeforeYou", label = "Nearest Totem Life" }, + { key = "vaalRejuvenationTotems", life = "TotalVaalRejuvenationTotemLife", mitigation = "VaalRejuvenationTotemAllyDamageMitigation", redirect = "takenFromVaalRejuvenationTotemsBeforeYou", label = "Total Vaal Rejuvenation Totem Life" }, + { key = "voidSpawn", life = "TotalVoidSpawnLife", mitigation = "VoidSpawnAllyDamageMitigation", redirect = "takenFromVoidSpawnBeforeYou", label = "Total Void Spawn Life" }, + { key = "stoneGolem", life = "TotalStoneGolemLife", mitigation = "StoneGolemAllyDamageMitigation", redirect = "takenFromStoneGolemBeforeYou", meleeOnly = true, label = "Total Stone Golem Life" }, + { key = "soulLink", life = "AlliedEnergyShield", mitigation = "SoulLinkMitigation", label = "Total Allied Energy shield" }, + { key = "frostShield", life = "FrostShieldLife", mitigation = "FrostShieldDamageMitigation", label = "Frost Shield Life" }, +} + +local function buildAllyLifePools(output) + local pools = { } + for _, ally in ipairs(allyLifePoolList) do + local life = output[ally.life] + local mitigation = output[ally.mitigation] + if life and life > 0 and mitigation and mitigation > 0 then + pools[ally.key] = { remaining = life, percent = m_min(mitigation, 100) / 100 } + end + end + return pools +end + -- Calculate hit chance function calcs.hitChance(evasion, accuracy) if accuracy < 0 then @@ -173,31 +198,7 @@ function calcs.reducePoolsByDamage(poolTable, damageTable, actor) local alliesTakenBeforeYou = poolTbl.AlliesTakenBeforeYou if not alliesTakenBeforeYou then - alliesTakenBeforeYou = {} - if output.FrostShieldLife then - alliesTakenBeforeYou["frostShield"] = { remaining = output.FrostShieldLife, percent = output.FrostShieldDamageMitigation / 100 } - end - if output.TotalMinionLife then - alliesTakenBeforeYou["minion"] = { remaining = output.TotalMinionLife, percent = output.MinionAllyDamageMitigation / 100 } - end - if output.TotalSpectreLife then - alliesTakenBeforeYou["spectres"] = { remaining = output.TotalSpectreLife, percent = output.SpectreAllyDamageMitigation / 100 } - end - if output.TotalTotemLife then - alliesTakenBeforeYou["totems"] = { remaining = output.TotalTotemLife, percent = output.TotemAllyDamageMitigation / 100 } - end - if output.TotalVaalRejuvenationTotemLife then - alliesTakenBeforeYou["vaalRejuvenationTotems"] = { remaining = output.TotalVaalRejuvenationTotemLife, percent = output.VaalRejuvenationTotemAllyDamageMitigation / 100 } - end - if output.TotalRadianceSentinelLife then - alliesTakenBeforeYou["radianceSentinel"] = { remaining = output.TotalRadianceSentinelLife, percent = output.RadianceSentinelAllyDamageMitigation / 100 } - end - if output.TotalVoidSpawnLife then - alliesTakenBeforeYou["voidSpawn"] = { remaining = output.TotalVoidSpawnLife, percent = output.VoidSpawnAllyDamageMitigation / 100 } - end - if output.AlliedEnergyShield then - alliesTakenBeforeYou["soulLink"] = { remaining = output.AlliedEnergyShield, percent = output.SoulLinkMitigation / 100 } - end + alliesTakenBeforeYou = buildAllyLifePools(output) end local damageTakenThatCanBeRecouped = poolTbl.damageTakenThatCanBeRecouped or { } @@ -262,13 +263,14 @@ function calcs.reducePoolsByDamage(poolTable, damageTable, actor) for _, damageType in ipairs(dmgTypeList) do local damageRemainder = damageTable[damageType] if damageRemainder then - for ally, allyValues in pairs(alliesTakenBeforeYou) do - if not allyValues.damageType or allyValues.damageType == damageType then + for _, ally in ipairs(allyLifePoolList) do + local allyValues = alliesTakenBeforeYou[ally.key] + if allyValues and (not allyValues.damageType or allyValues.damageType == damageType) then if allyValues.remaining > 0 then local tempDamage = m_min(damageRemainder * allyValues.percent, allyValues.remaining) allyValues.remaining = m_floor(allyValues.remaining - tempDamage) damageRemainder = damageRemainder - tempDamage - resourcesLostToTypeDamage[damageType][ally] = tempDamage >= 1 and tempDamage or nil + resourcesLostToTypeDamage[damageType][ally.key] = tempDamage >= 1 and tempDamage or nil end end end @@ -425,29 +427,12 @@ end ---@return table breakdownTable with drained resource list local function incomingDamageBreakdown(breakdownTable, poolsRemaining, output) --region Breakdown inserts - if output.FrostShieldLife and output.FrostShieldLife > 0 then - t_insert(breakdownTable, s_format("\t%d "..colorCodes.GEM.."Frost Shield Life ^7(%d remaining)", output.FrostShieldLife - poolsRemaining.AlliesTakenBeforeYou["frostShield"].remaining, poolsRemaining.AlliesTakenBeforeYou["frostShield"].remaining)) - end - if output.TotalMinionLife and output.TotalMinionLife > 0 then - t_insert(breakdownTable, s_format("\t%d "..colorCodes.GEM.."Total Minion Life ^7(%d remaining)", output.TotalMinionLife - poolsRemaining.AlliesTakenBeforeYou["minion"].remaining, poolsRemaining.AlliesTakenBeforeYou["minion"].remaining)) - end - if output.TotalSpectreLife and output.TotalSpectreLife > 0 then - t_insert(breakdownTable, s_format("\t%d "..colorCodes.GEM.."Total Spectre Life ^7(%d remaining)", output.TotalSpectreLife - poolsRemaining.AlliesTakenBeforeYou["spectres"].remaining, poolsRemaining.AlliesTakenBeforeYou["spectres"].remaining)) - end - if output.TotalTotemLife and output.TotalTotemLife > 0 then - t_insert(breakdownTable, s_format("\t%d "..colorCodes.GEM.."Total Totem Life ^7(%d remaining)", output.TotalTotemLife - poolsRemaining.AlliesTakenBeforeYou["totems"].remaining, poolsRemaining.AlliesTakenBeforeYou["totems"].remaining)) - end - if output.TotalVaalRejuvenationTotemLife and output.TotalVaalRejuvenationTotemLife > 0 then - t_insert(breakdownTable, s_format("\t%d "..colorCodes.GEM.."Total Vaal Rejuvenation Totem Life ^7(%d remaining)", output.TotalVaalRejuvenationTotemLife - poolsRemaining.AlliesTakenBeforeYou["vaalRejuvenationTotems"].remaining, poolsRemaining.AlliesTakenBeforeYou["vaalRejuvenationTotems"].remaining)) - end - if output.TotalRadianceSentinelLife and output.TotalRadianceSentinelLife > 0 then - t_insert(breakdownTable, s_format("\t%d "..colorCodes.GEM.."Total Sentinel of Radiance Life ^7(%d remaining)", output.TotalRadianceSentinelLife - poolsRemaining.AlliesTakenBeforeYou["radianceSentinel"].remaining, poolsRemaining.AlliesTakenBeforeYou["radianceSentinel"].remaining)) - end - if output.TotalVoidSpawnLife and output.TotalVoidSpawnLife > 0 then - t_insert(breakdownTable, s_format("\t%d "..colorCodes.GEM.."Total Void Spawn Life ^7(%d remaining)", output.TotalVoidSpawnLife - poolsRemaining.AlliesTakenBeforeYou["voidSpawn"].remaining, poolsRemaining.AlliesTakenBeforeYou["voidSpawn"].remaining)) - end - if output.AlliedEnergyShield and output.AlliedEnergyShield > 0 then - t_insert(breakdownTable, s_format("\t%d "..colorCodes.GEM.."Total Allied Energy shield ^7(%d remaining)", output.AlliedEnergyShield - poolsRemaining.AlliesTakenBeforeYou["soulLink"].remaining, poolsRemaining.AlliesTakenBeforeYou["soulLink"].remaining)) + for _, ally in ipairs(allyLifePoolList) do + local pool = poolsRemaining.AlliesTakenBeforeYou[ally.key] + if pool then + local life = output[ally.life] + t_insert(breakdownTable, s_format("\t%d "..colorCodes.GEM..ally.label.." ^7(%d remaining)", life - pool.remaining, pool.remaining)) + end end for _, damageType in ipairs(dmgTypeList) do if poolsRemaining.resourcesLostToTypeDamage[damageType].aegis then @@ -2466,40 +2451,63 @@ function calcs.buildDefenceEstimations(env, actor) } end - -- from Minion - output["MinionAllyDamageMitigation"] = modDB:Sum("BASE", nil, "takenFromMinionBeforeYou") - if output["MinionAllyDamageMitigation"] ~= 0 then - output["TotalMinionLife"] = modDB:Sum("BASE", nil, "Multiplier:MinionLife") - end - - -- from spectres - output["SpectreAllyDamageMitigation"] = modDB:Sum("BASE", nil, "takenFromSpectresBeforeYou") - if output["SpectreAllyDamageMitigation"] ~= 0 then - output["TotalSpectreLife"] = modDB:Sum("BASE", nil, "TotalSpectreLife") - end - - -- from totems - output["TotemAllyDamageMitigation"] = modDB:Sum("BASE", nil, "takenFromTotemsBeforeYou") - if output["TotemAllyDamageMitigation"] ~= 0 then - output["TotalTotemLife"] = modDB:Sum("BASE", nil, "TotalTotemLife") + -- Every ally redirect uses the same pool rules; the Safeguarding Golem is the only melee-only case. + for _, ally in ipairs(allyLifePoolList) do + if ally.redirect then + local mitigation = modDB:Sum("BASE", nil, ally.redirect) + if ally.meleeOnly then + mitigation = damageCategoryConfig == "Melee" and mitigation or damageCategoryConfig == "Average" and mitigation / 4 or 0 + end + output[ally.mitigation] = mitigation + if mitigation ~= 0 then + local life = modDB:Sum("BASE", nil, ally.life) + if life == 0 and ally.fallback then + life = modDB:Sum("BASE", nil, ally.fallback) + end + output[ally.life] = modDB:Override(nil, ally.life) or life + end + end end - - -- from VaalRejuveTotem - output["VaalRejuvenationTotemAllyDamageMitigation"] = modDB:Sum("BASE", nil, "takenFromVaalRejuvenationTotemsBeforeYou") + output["TotemAllyDamageMitigation"] - if output["VaalRejuvenationTotemAllyDamageMitigation"] ~= output["TotemAllyDamageMitigation"] then - output["TotalVaalRejuvenationTotemLife"] = modDB:Sum("BASE", nil, "TotalVaalRejuvenationTotemLife") + + -- Companionship and an ally-specific redirect can both spend the same minion's Life. + local sharedMinionLifeOverride + for _, ally in ipairs(allyLifePoolList) do + if modDB:Flag(nil, "MinionLifeShares"..ally.life) then + local specificOverride = modDB:Override(nil, ally.life) + if not modDB:Override(nil, "TotalMinionLife") and specificOverride ~= nil then + output.TotalMinionLife = specificOverride + sharedMinionLifeOverride = true + elseif not output.TotalMinionLife or output.TotalMinionLife == 0 then + output.TotalMinionLife = output[ally.life] + end + output.MinionAllyDamageMitigation = output.MinionAllyDamageMitigation + (output[ally.mitigation] or 0) + output[ally.mitigation] = 0 + output[ally.life] = nil + end end - - -- from Sentinel of Radiance - output["RadianceSentinelAllyDamageMitigation"] = modDB:Sum("BASE", nil, "takenFromRadianceSentinelBeforeYou") - if output["RadianceSentinelAllyDamageMitigation"] ~= 0 then - output["TotalRadianceSentinelLife"] = modDB:Sum("BASE", nil, "TotalRadianceSentinelLife") + + -- When Vaal Rejuvenation is treated as the nearest Totem, both redirects use its Life. + if (output.TotemAllyDamageMitigation or 0) > 0 and (output.TotalTotemLife or 0) == 0 and (output.TotalVaalRejuvenationTotemLife or 0) > 0 then + output.VaalRejuvenationTotemAllyDamageMitigation = output.VaalRejuvenationTotemAllyDamageMitigation + output.TotemAllyDamageMitigation + output.TotemAllyDamageMitigation = 0 + output.TotalTotemLife = nil end - - -- from Void Spawn - output["VoidSpawnAllyDamageMitigation"] = modDB:Sum("BASE", nil, "takenFromVoidSpawnBeforeYou") - if output["VoidSpawnAllyDamageMitigation"] ~= 0 then - output["TotalVoidSpawnLife"] = modDB:Sum("BASE", nil, "TotalVoidSpawnLife") + + if breakdown then + for _, ally in ipairs(allyLifePoolList) do + local lifeList = actor.allyLifeList and actor.allyLifeList[ally.life] + local override = modDB:Override(nil, ally.life) or ally.life == "TotalMinionLife" and sharedMinionLifeOverride and output.TotalMinionLife + if output[ally.life] and (lifeList or override) then + breakdown[ally.life] = { } + if override then + t_insert(breakdown[ally.life], s_format("%d ^8(from config)", output[ally.life])) + else + for _, entry in ipairs(lifeList) do + t_insert(breakdown[ally.life], s_format("%d ^8(%s%s)", entry.life, entry.count and entry.count > 1 and entry.count.."x " or "", entry.name)) + end + end + end + end end -- from Allied Energy Shield @@ -2588,31 +2596,7 @@ function calcs.buildDefenceEstimations(env, actor) aegis[damageType] = output[damageType.."Aegis"] or 0 guard[damageType] = output[damageType.."GuardAbsorb"] or 0 end - local alliesTakenBeforeYou = {} - if output.FrostShieldLife then - alliesTakenBeforeYou["frostShield"] = { remaining = output.FrostShieldLife, percent = output.FrostShieldDamageMitigation / 100 } - end - if output.TotalMinionLife then - alliesTakenBeforeYou["minion"] = { remaining = output.TotalMinionLife, percent = output.MinionAllyDamageMitigation / 100 } - end - if output.TotalSpectreLife then - alliesTakenBeforeYou["spectres"] = { remaining = output.TotalSpectreLife, percent = output.SpectreAllyDamageMitigation / 100 } - end - if output.TotalTotemLife then - alliesTakenBeforeYou["totems"] = { remaining = output.TotalTotemLife, percent = output.TotemAllyDamageMitigation / 100 } - end - if output.TotalVaalRejuvenationTotemLife then - alliesTakenBeforeYou["vaalRejuvenationTotems"] = { remaining = output.TotalVaalRejuvenationTotemLife, percent = output.VaalRejuvenationTotemAllyDamageMitigation / 100 } - end - if output.TotalRadianceSentinelLife then - alliesTakenBeforeYou["radianceSentinel"] = { remaining = output.TotalRadianceSentinelLife, percent = output.RadianceSentinelAllyDamageMitigation / 100 } - end - if output.TotalVoidSpawnLife then - alliesTakenBeforeYou["voidSpawn"] = { remaining = output.TotalVoidSpawnLife, percent = output.VoidSpawnAllyDamageMitigation / 100 } - end - if output.AlliedEnergyShield then - alliesTakenBeforeYou["soulLink"] = { remaining = output.AlliedEnergyShield, percent = output.SoulLinkMitigation / 100 } - end + local alliesTakenBeforeYou = buildAllyLifePools(output) local poolTable = { AlliesTakenBeforeYou = alliesTakenBeforeYou, @@ -3156,43 +3140,20 @@ function calcs.buildDefenceEstimations(env, actor) output[damageType.."TotalHitPool"] = m_max(output[damageType.."TotalHitPool"] - poolProtected, 0) + m_min(output[damageType.."TotalHitPool"], poolProtected) / (1 - GuardAbsorbRate / 100) end end - -- from allies before you - -- frost shield - if output["FrostShieldLife"] > 0 then - local poolProtected = output["FrostShieldLife"] / (output["FrostShieldDamageMitigation"] / 100) * (1 - output["FrostShieldDamageMitigation"] / 100) - output[damageType.."TotalHitPool"] = m_max(output[damageType.."TotalHitPool"] - poolProtected, 0) + m_min(output[damageType.."TotalHitPool"], poolProtected) / (1 - output["FrostShieldDamageMitigation"] / 100) - end - -- minions - if output["TotalMinionLife"] and output["TotalMinionLife"] > 0 then - local poolProtected = output["TotalMinionLife"] / (output["MinionAllyDamageMitigation"] / 100) * (1 - output["MinionAllyDamageMitigation"] / 100) - output[damageType.."TotalHitPool"] = m_max(output[damageType.."TotalHitPool"] - poolProtected, 0) + m_min(output[damageType.."TotalHitPool"], poolProtected) / (1 - output["MinionAllyDamageMitigation"] / 100) - end - -- spectres - if output["TotalSpectreLife"] and output["TotalSpectreLife"] > 0 then - local poolProtected = output["TotalSpectreLife"] / (output["SpectreAllyDamageMitigation"] / 100) * (1 - output["SpectreAllyDamageMitigation"] / 100) - output[damageType.."TotalHitPool"] = m_max(output[damageType.."TotalHitPool"] - poolProtected, 0) + m_min(output[damageType.."TotalHitPool"], poolProtected) / (1 - output["SpectreAllyDamageMitigation"] / 100) - end - -- totems - if output["TotalTotemLife"] and output["TotalTotemLife"] > 0 then - local poolProtected = output["TotalTotemLife"] / (output["TotemAllyDamageMitigation"] / 100) * (1 - output["TotemAllyDamageMitigation"] / 100) - output[damageType.."TotalHitPool"] = m_max(output[damageType.."TotalHitPool"] - poolProtected, 0) + m_min(output[damageType.."TotalHitPool"], poolProtected) / (1 - output["TotemAllyDamageMitigation"] / 100) - end - if output["TotalVaalRejuvenationTotemLife"] and output["TotalVaalRejuvenationTotemLife"] > 0 then - local poolProtected = output["TotalVaalRejuvenationTotemLife"] / (output["VaalRejuvenationTotemAllyDamageMitigation"] / 100) * (1 - output["VaalRejuvenationTotemAllyDamageMitigation"] / 100) - output[damageType.."TotalHitPool"] = m_max(output[damageType.."TotalHitPool"] - poolProtected, 0) + m_min(output[damageType.."TotalHitPool"], poolProtected) / (1 - output["VaalRejuvenationTotemAllyDamageMitigation"] / 100) - end - if output["TotalRadianceSentinelLife"] and output["TotalRadianceSentinelLife"] > 0 then - local poolProtected = output["TotalRadianceSentinelLife"] / (output["RadianceSentinelAllyDamageMitigation"] / 100) * (1 - output["RadianceSentinelAllyDamageMitigation"] / 100) - output[damageType.."TotalHitPool"] = m_max(output[damageType.."TotalHitPool"] - poolProtected, 0) + m_min(output[damageType.."TotalHitPool"], poolProtected) / (1 - output["RadianceSentinelAllyDamageMitigation"] / 100) - end - if output["TotalVoidSpawnLife"] and output["TotalVoidSpawnLife"] > 0 then - local poolProtected = output["TotalVoidSpawnLife"] / (output["VoidSpawnAllyDamageMitigation"] / 100) * (1 - output["VoidSpawnAllyDamageMitigation"] / 100) - output[damageType.."TotalHitPool"] = m_max(output[damageType.."TotalHitPool"] - poolProtected, 0) + m_min(output[damageType.."TotalHitPool"], poolProtected) / (1 - output["VoidSpawnAllyDamageMitigation"] / 100) - end - -- soul link - if output["AlliedEnergyShield"] and output["AlliedEnergyShield"] > 0 then - local poolProtected = output["AlliedEnergyShield"] / (output["SoulLinkMitigation"] / 100) * (1 - output["SoulLinkMitigation"] / 100) - output[damageType.."TotalHitPool"] = m_max(output[damageType.."TotalHitPool"] - poolProtected, 0) + m_min(output[damageType.."TotalHitPool"], poolProtected) / (1 - output["SoulLinkMitigation"] / 100) + -- Undo the ally pool drains in reverse order to recover the incoming hit. + for index = #allyLifePoolList, 1, -1 do + local ally = allyLifePoolList[index] + local life = output[ally.life] + local mitigation = output[ally.mitigation] + if life and life > 0 and mitigation and mitigation > 0 then + mitigation = m_min(mitigation, 100) + if mitigation == 100 then + output[damageType.."TotalHitPool"] = output[damageType.."TotalHitPool"] + life + else + local poolProtected = life / (mitigation / 100) * (1 - mitigation / 100) + output[damageType.."TotalHitPool"] = m_max(output[damageType.."TotalHitPool"] - poolProtected, 0) + m_min(output[damageType.."TotalHitPool"], poolProtected) / (1 - mitigation / 100) + end + end end end @@ -3804,37 +3765,12 @@ function calcs.buildDefenceEstimations(env, actor) t_insert(breakdownTable, s_format("= %.1f", output[damageType.."TakenHit"])) t_insert(breakdownTable, "This part of the hit drains the following resources:") - if resourcesLost.frostShield then - resourcesLostSum = resourcesLostSum + resourcesLost.frostShield - t_insert(breakdownTable, s_format("\t%d "..colorCodes.GEM.."Frost Shield Life", resourcesLost.frostShield)) - end - if resourcesLost.minion then - resourcesLostSum = resourcesLostSum + resourcesLost.minion - t_insert(breakdownTable, s_format("\t%d "..colorCodes.GEM.."Total Minion Life", resourcesLost.minion)) - end - if resourcesLost.spectres then - resourcesLostSum = resourcesLostSum + resourcesLost.spectres - t_insert(breakdownTable, s_format("\t%d "..colorCodes.GEM.."Total Spectre Life", resourcesLost.spectres)) - end - if resourcesLost.totems then - resourcesLostSum = resourcesLostSum + resourcesLost.totems - t_insert(breakdownTable, s_format("\t%d "..colorCodes.GEM.."Total Totem Life", resourcesLost.totems)) - end - if resourcesLost.vaalRejuvenationTotems then - resourcesLostSum = resourcesLostSum + resourcesLost.vaalRejuvenationTotems - t_insert(breakdownTable, s_format("\t%d "..colorCodes.GEM.."Total Vaal Rejuvenation Totem Life", resourcesLost.vaalRejuvenationTotems)) - end - if resourcesLost.radianceSentinel then - resourcesLostSum = resourcesLostSum + resourcesLost.radianceSentinel - t_insert(breakdownTable, s_format("\t%d "..colorCodes.GEM.."Total Sentinel of Radiance Life", resourcesLost.radianceSentinel)) - end - if resourcesLost.voidSpawn then - resourcesLostSum = resourcesLostSum + resourcesLost.voidSpawn - t_insert(breakdownTable, s_format("\t%d "..colorCodes.GEM.."Total Void Spawn Life", resourcesLost.voidSpawn)) - end - if resourcesLost.soulLink then - resourcesLostSum = resourcesLostSum + resourcesLost.soulLink - t_insert(breakdownTable, s_format("\t%d "..colorCodes.GEM.."Total Allied Energy shield", resourcesLost.soulLink)) + for _, ally in ipairs(allyLifePoolList) do + local lost = resourcesLost[ally.key] + if lost then + resourcesLostSum = resourcesLostSum + lost + t_insert(breakdownTable, s_format("\t%d "..colorCodes.GEM..ally.label, lost)) + end end if resourcesLost.aegis then resourcesLostSum = resourcesLostSum + resourcesLost.aegis diff --git a/src/Modules/CalcOffence.lua b/src/Modules/CalcOffence.lua index b04e01012f6..2ee3ed9cb2d 100644 --- a/src/Modules/CalcOffence.lua +++ b/src/Modules/CalcOffence.lua @@ -310,6 +310,13 @@ function calcSkillDuration(skillModList, skillCfg, skillData, env, enemyDB) return duration end +-- Keep defensive Totem pools in step with the Totem Life shown in the skill calculations. +function calcs.calcTotemLife(env, activeSkill) + local lifeMod = calcLib.mod(activeSkill.skillModList, activeSkill.skillCfg, "TotemLife") + local life = round(m_floor(env.data.monsterAllyLifeTable[activeSkill.skillData.totemLevel] * env.data.totemLifeMult[activeSkill.skillTotemId]) * lifeMod) + return life, lifeMod +end + -- Performs all offensive calculations function calcs.offence(env, actor, activeSkill) local modDB = actor.modDB @@ -1387,8 +1394,7 @@ function calcs.offence(env, actor, activeSkill) "Totems Summoned: "..output.TotemsSummoned..(env.configInput.TotemsSummoned and " ^8(overridden from the Configuration tab)" or " ^8(can be overridden in the Configuration tab)"), } end - output.TotemLifeMod = calcLib.mod(skillModList, skillCfg, "TotemLife") - output.TotemLife = round(m_floor(env.data.monsterAllyLifeTable[skillData.totemLevel] * env.data.totemLifeMult[activeSkill.skillTotemId]) * output.TotemLifeMod) + output.TotemLife, output.TotemLifeMod = calcs.calcTotemLife(env, activeSkill) output.TotemEnergyShield = skillModList:Sum("BASE", skillCfg, "TotemEnergyShield") output.TotemBlockChance = skillModList:Sum("BASE", skillCfg, "TotemBlockChance") output.TotemArmour = skillModList:Sum("BASE", skillCfg, "TotemArmour") diff --git a/src/Modules/CalcPerform.lua b/src/Modules/CalcPerform.lua index 1fb88582c4c..96c85b8274d 100644 --- a/src/Modules/CalcPerform.lua +++ b/src/Modules/CalcPerform.lua @@ -1083,6 +1083,127 @@ function calcs.actionSpeedMod(actor) return actionSpeedMod end +-- Build the same modifier database for the selected minion and any minion used as a defensive Life pool. +local function initMinionModDB(env, activeSkill, output) + local modDB = env.modDB + local minion = activeSkill.minion + minion.output = output or { } + minion.modDB.multipliers["Level"] = minion.level + calcs.initModDB(env, minion.modDB) + local baseLife = minion.lifeTable[minion.level] * minion.minionData.life + if minion.hostile then + baseLife = baseLife * (env.data.mapLevelLifeMult[env.enemyLevel] or 1) + end + minion.modDB:NewMod("Life", "BASE", m_floor(baseLife), "Base") + if minion.minionData.energyShield then + local esTable = minion.hostile and minion.lifeTable or env.data.monsterAllyLifeTable + local baseES = esTable[minion.level] * minion.minionData.life * minion.minionData.energyShield + if minion.hostile then + baseES = baseES * (env.data.mapLevelLifeMult[env.enemyLevel] or 1) + end + minion.modDB:NewMod("EnergyShield", "BASE", m_floor(baseES), "Base") + end + minion.modDB:NewMod("Armour", "BASE", round(env.data.monsterArmourTable[minion.level] * (minion.minionData.armour or 1)), "Base") + minion.modDB:NewMod("Evasion", "BASE", round(env.data.monsterEvasionTable[minion.level] * (minion.minionData.evasion or 1)), "Base") + if modDB:Flag(nil, "MinionAccuracyEqualsAccuracy") then + minion.modDB:NewMod("Accuracy", "BASE", calcLib.val(modDB, "Accuracy") + calcLib.val(modDB, "Dex") * (modDB:Override(nil, "DexAccBonusOverride") or data.misc.AccuracyPerDexBase), "Player") + else + minion.modDB:NewMod("CannotBeEvaded", "FLAG", 1, "Minion Attacks always hit") + end + minion.modDB:NewMod("CritMultiplier", "BASE", env.data.monsterConstants["base_critical_strike_multiplier"] - 100, "Base") + minion.modDB:NewMod("DotMultiplier", "BASE", env.data.monsterConstants["critical_ailment_dot_multiplier_+"], "Base", { type = "Condition", var = "CriticalStrike" }) + minion.modDB:NewMod("FireResist", "BASE", minion.minionData.fireResist, "Base") + minion.modDB:NewMod("ColdResist", "BASE", minion.minionData.coldResist, "Base") + minion.modDB:NewMod("LightningResist", "BASE", minion.minionData.lightningResist, "Base") + minion.modDB:NewMod("ChaosResist", "BASE", minion.minionData.chaosResist, "Base") + minion.modDB:NewMod("CritChance", "INC", env.data.monsterConstants["critical_strike_chance_+%_per_power_charge"], "Base", { type = "Multiplier", var = "PowerCharge" }) + minion.modDB:NewMod("Speed", "INC", env.data.monsterConstants["base_attack_speed_+%_per_frenzy_charge"], "Base", ModFlag.Attack, { type = "Multiplier", var = "FrenzyCharge" }) + minion.modDB:NewMod("Speed", "INC", env.data.monsterConstants["base_cast_speed_+%_per_frenzy_charge"], "Base", ModFlag.Cast, { type = "Multiplier", var = "FrenzyCharge" }) + minion.modDB:NewMod("Damage", "MORE", env.data.monsterConstants["object_inherent_damage_+%_final_per_frenzy_charge"], "Base", { type = "Multiplier", var = "FrenzyCharge" }) + minion.modDB:NewMod("PhysicalDamageReduction", "BASE", env.data.monsterConstants["physical_damage_reduction_%_per_endurance_charge"], "Base", { type = "Multiplier", var = "EnduranceCharge" }) + minion.modDB:NewMod("ElementalDamageReduction", "BASE", env.data.monsterConstants["elemental_damage_reduction_%_per_endurance_charge_if_player_minion"], "Base", { type = "Multiplier", var = "EnduranceCharge" }) + minion.modDB:NewMod("ProjectileCount", "BASE", 1, "Base") + minion.modDB:NewMod("MaximumFortification", "BASE", env.data.monsterConstants["base_max_fortification"], "Base") + minion.modDB:NewMod("Damage", "MORE", 200, "Base", 0, KeywordFlag.Bleed, { type = "ActorCondition", actor = "enemy", var = "Moving" }) + for _, mod in ipairs(minion.minionData.modList) do + minion.modDB:AddMod(mod) + end + for _, mod in ipairs(activeSkill.extraSkillModList) do + minion.modDB:AddMod(mod) + end + if env.aegisModList then + minion.itemList["Weapon 3"] = env.aegisItem + minion.modDB:AddList(env.aegisModList) + end + if env.theIronMass and minion.type == "RaisedSkeleton" then + minion.modDB:AddList(env.theIronMass) + end + if activeSkill.skillData.minionUseBowAndQuiver then + if env.player.weaponData1.type == "Bow" then + minion.modDB:AddList(env.player.itemList["Weapon 1"].slotModList[1]) + end + if env.player.itemList["Weapon 2"] and env.player.itemList["Weapon 2"].type == "Quiver" then + minion.modDB:ScaleAddList(env.player.itemList["Weapon 2"].modList, m_max(modDB:Sum("BASE", nil, "WidowHailMultiplier"), 1)) + end + if modDB:Flag(nil, "BlinkAndMirrorUseGloves") and env.player.itemList["Gloves"] then + minion.modDB:AddList(env.player.itemList["Gloves"].modList) + end + end + if activeSkill.skillData.minionUseMainHandWeapon then + minion.modDB:AddList(env.player.itemList["Weapon 1"].slotModList[1]) + end + if minion.itemSet or minion.uses then + for slotName, slot in pairs(env.build.itemsTab.slots) do + if minion.uses[slotName] then + local item + if minion.itemSet then + if slot.weaponSet == 1 and minion.itemSet.useSecondWeaponSet then + slotName = slotName .. " Swap" + end + item = env.build.itemsTab.items[minion.itemSet[slotName].selItemId] + else + item = env.player.itemList[slotName] + end + if item then + minion.itemList[slotName] = item + minion.modDB:AddList(item.modList or item.slotModList[slot.slotNum]) + end + end + end + end + if modDB:Sum("BASE", nil, "StrengthAddedToMinions") > 0 then + minion.modDB:NewMod("Str", "BASE", round(calcLib.val(modDB, "Str") * modDB:Sum("BASE", nil, "StrengthAddedToMinions") / 100), "Player") + end +end + +-- Add modifiers granted to the minion by its skill or an active buff. +local function addMinionModifiers(modList, skillCfg, minion) + for _, value in ipairs(modList:List(skillCfg, "MinionModifier")) do + if not value.type or minion.type == value.type then + minion.modDB:AddMod(value.mod) + end + end +end + +-- These are the minions whose Life can be spent before the player's. +local minionLifePoolBySkill = { + ["Summon Sentinel of Radiance"] = { life = "TotalRadianceSentinelLife", redirect = "takenFromRadianceSentinelBeforeYou", source = "Sentinel of Radiance" }, + ["Summon Void Spawn"] = { life = "TotalVoidSpawnLife", redirect = "takenFromVoidSpawnBeforeYou", source = "Void Spawns", limit = "ActiveVoidSpawnLimit" }, + ["Summon Stone Golem of Safeguarding"] = { life = "TotalStoneGolemLife", redirect = "takenFromStoneGolemBeforeYou", source = "Stone Golem" }, +} +local spectreLifePool = { life = "TotalSpectreLife", redirect = "takenFromSpectresBeforeYou", source = "Spectres", limit = "ActiveSpectreLimit" } +local companionshipLifePool = { life = "TotalMinionLife", redirect = "takenFromMinionBeforeYou", source = "Minion" } +local totemLifePoolByStat = { + TotalTotemLife = { redirect = "takenFromTotemsBeforeYou", source = "Totem" }, + TotalVaalRejuvenationTotemLife = { redirect = "takenFromVaalRejuvenationTotemsBeforeYou", source = "Vaal Rejuvenation Totem" }, +} +local allyLifeRedirects = { spectreLifePool.redirect, companionshipLifePool.redirect } +for _, lifePools in pairs({ minionLifePoolBySkill, totemLifePoolByStat }) do + for _, pool in pairs(lifePools) do + t_insert(allyLifeRedirects, pool.redirect) + end +end + -- Finalises the environment and performs the stat calculations: -- 1. Merges keystone modifiers -- 2. Initialises minion skills @@ -1122,100 +1243,13 @@ function calcs.perform(env, skipEHP) env.player.partyMembers = env.partyMembers local partyTabEnableExportBuffs = env.build.partyTab.enableExportBuffs and env.mode ~= "CALCULATOR" + -- Calculator passes reuse the environment after the player's shield has been removed. + env.aegisItem = env.aegisModList and (env.player.itemList["Weapon 2"] or env.aegisItem) env.minion = env.player.mainSkill.minion if env.minion then -- Initialise minion modifier database output.Minion = { } - env.minion.output = output.Minion - env.minion.modDB.multipliers["Level"] = env.minion.level - calcs.initModDB(env, env.minion.modDB) - local baseLife = env.minion.lifeTable[env.minion.level] * env.minion.minionData.life - if env.minion.hostile then - baseLife = baseLife * (env.data.mapLevelLifeMult[env.enemyLevel] or 1) - end - env.minion.modDB:NewMod("Life", "BASE", m_floor(baseLife), "Base") - if env.minion.minionData.energyShield then - local esTable = env.minion.hostile and env.minion.lifeTable or env.data.monsterAllyLifeTable - local baseES = esTable[env.minion.level] * env.minion.minionData.life * env.minion.minionData.energyShield - if env.minion.hostile then - baseES = baseES * (env.data.mapLevelLifeMult[env.enemyLevel] or 1) - end - env.minion.modDB:NewMod("EnergyShield", "BASE", m_floor(baseES), "Base") - end - --Armour formula is math.floor((10 + 2 * level) * 1.067 ^ level) - env.minion.modDB:NewMod("Armour", "BASE", round(env.data.monsterArmourTable[env.minion.level] * (env.minion.minionData.armour or 1)), "Base") - --Evasion formula is math.floor((50 + 16 * level + 16 * level * (MonsterType.Evasion / 100)) * (1.0212 ^ level) - env.minion.modDB:NewMod("Evasion", "BASE", round(env.data.monsterEvasionTable[env.minion.level] * (env.minion.minionData.evasion or 1)), "Base") - if modDB:Flag(nil, "MinionAccuracyEqualsAccuracy") then - env.minion.modDB:NewMod("Accuracy", "BASE", calcLib.val(modDB, "Accuracy") + calcLib.val(modDB, "Dex") * (modDB:Override(nil, "DexAccBonusOverride") or data.misc.AccuracyPerDexBase), "Player") - else - -- Minion Attacks now inherently always hit (Patch 3.27) - env.minion.modDB:NewMod("CannotBeEvaded", "FLAG", 1, "Minion Attacks always hit") - end - env.minion.modDB:NewMod("CritMultiplier", "BASE", env.data.monsterConstants["base_critical_strike_multiplier"] - 100, "Base") - env.minion.modDB:NewMod("DotMultiplier", "BASE", env.data.monsterConstants["critical_ailment_dot_multiplier_+"], "Base", { type = "Condition", var = "CriticalStrike" }) - env.minion.modDB:NewMod("FireResist", "BASE", env.minion.minionData.fireResist, "Base") - env.minion.modDB:NewMod("ColdResist", "BASE", env.minion.minionData.coldResist, "Base") - env.minion.modDB:NewMod("LightningResist", "BASE", env.minion.minionData.lightningResist, "Base") - env.minion.modDB:NewMod("ChaosResist", "BASE", env.minion.minionData.chaosResist, "Base") - env.minion.modDB:NewMod("CritChance", "INC", env.data.monsterConstants["critical_strike_chance_+%_per_power_charge"], "Base", { type = "Multiplier", var = "PowerCharge" }) - env.minion.modDB:NewMod("Speed", "INC", env.data.monsterConstants["base_attack_speed_+%_per_frenzy_charge"], "Base", ModFlag.Attack, { type = "Multiplier", var = "FrenzyCharge" }) - env.minion.modDB:NewMod("Speed", "INC", env.data.monsterConstants["base_cast_speed_+%_per_frenzy_charge"], "Base", ModFlag.Cast, { type = "Multiplier", var = "FrenzyCharge" }) - env.minion.modDB:NewMod("Damage", "MORE", env.data.monsterConstants["object_inherent_damage_+%_final_per_frenzy_charge"], "Base", { type = "Multiplier", var = "FrenzyCharge" }) - env.minion.modDB:NewMod("PhysicalDamageReduction", "BASE", env.data.monsterConstants["physical_damage_reduction_%_per_endurance_charge"], "Base", { type = "Multiplier", var = "EnduranceCharge" }) - env.minion.modDB:NewMod("ElementalDamageReduction", "BASE", env.data.monsterConstants["elemental_damage_reduction_%_per_endurance_charge_if_player_minion"], "Base", { type = "Multiplier", var = "EnduranceCharge" }) - env.minion.modDB:NewMod("ProjectileCount", "BASE", 1, "Base") - env.minion.modDB:NewMod("MaximumFortification", "BASE", env.data.monsterConstants["base_max_fortification"], "Base") - env.minion.modDB:NewMod("Damage", "MORE", 200, "Base", 0, KeywordFlag.Bleed, { type = "ActorCondition", actor = "enemy", var = "Moving" }) - for _, mod in ipairs(env.minion.minionData.modList) do - env.minion.modDB:AddMod(mod) - end - for _, mod in ipairs(env.player.mainSkill.extraSkillModList) do - env.minion.modDB:AddMod(mod) - end - if env.aegisModList then - env.minion.itemList["Weapon 3"] = env.player.itemList["Weapon 2"] - env.minion.modDB:AddList(env.aegisModList) - end - if env.theIronMass and env.minion.type == "RaisedSkeleton" then - env.minion.modDB:AddList(env.theIronMass) - end - if env.player.mainSkill.skillData.minionUseBowAndQuiver then - if env.player.weaponData1.type == "Bow" then - env.minion.modDB:AddList(env.player.itemList["Weapon 1"].slotModList[1]) - end - if env.player.itemList["Weapon 2"] and env.player.itemList["Weapon 2"].type == "Quiver" then - env.minion.modDB:ScaleAddList(env.player.itemList["Weapon 2"].modList, m_max(modDB:Sum("BASE", nil, "WidowHailMultiplier"), 1)) - end - if modDB:Flag(nil, "BlinkAndMirrorUseGloves") and env.player.itemList["Gloves"] then - env.minion.modDB:AddList(env.player.itemList["Gloves"].modList) - end - end - if env.player.mainSkill.skillData.minionUseMainHandWeapon then - env.minion.modDB:AddList(env.player.itemList["Weapon 1"].slotModList[1]) - end - if env.minion.itemSet or env.minion.uses then - for slotName, slot in pairs(env.build.itemsTab.slots) do - if env.minion.uses[slotName] then - local item - if env.minion.itemSet then - if slot.weaponSet == 1 and env.minion.itemSet.useSecondWeaponSet then - slotName = slotName .. " Swap" - end - item = env.build.itemsTab.items[env.minion.itemSet[slotName].selItemId] - else - item = env.player.itemList[slotName] - end - if item then - env.minion.itemList[slotName] = item - env.minion.modDB:AddList(item.modList or item.slotModList[slot.slotNum]) - end - end - end - end - if modDB:Sum("BASE", nil, "StrengthAddedToMinions") > 0 then - env.minion.modDB:NewMod("Str", "BASE", round(calcLib.val(modDB, "Str") * modDB:Sum("BASE", nil, "StrengthAddedToMinions") / 100), "Player") - end + initMinionModDB(env, env.player.mainSkill, output.Minion) end if env.aegisModList then env.player.itemList["Weapon 2"] = nil @@ -1249,7 +1283,7 @@ function calcs.perform(env, skipEHP) applyEnemyModifiers(env.minion, true) end applyEnemyModifiers(env.enemy, true) - local minionCount = {} + local minionCounts = { } for _, activeSkill in ipairs(env.player.activeSkillList) do if activeSkill.skillTypes[SkillType.Brand] then @@ -1329,16 +1363,26 @@ function calcs.perform(env, skipEHP) hasGuaranteedBonechill = true end end - if activeSkill.minion and activeSkill.minion.minionData and activeSkill.minion.minionData.limit then - local limit = m_floor(modDB:Override(nil, activeSkill.minion.minionData.limit) or (calcLib.val(activeSkill.skillModList, activeSkill.minion.minionData.limit) * activeSkill.skillModList:More(activeSkill.skillCfg, "ActiveMinionLimit"))) - output[activeSkill.minion.minionData.limit] = m_max(limit, output[activeSkill.minion.minionData.limit] or 0) - if not minionCount[activeSkill.minion.minionData.limit] then - env.player.modDB:NewMod("Multiplier:SummonedMinion", "BASE", output[activeSkill.minion.minionData.limit], "Config", { type = "Condition", var = "Combat" }) - if not activeSkill.skillTypes[SkillType.Vaal] then - env.player.modDB:NewMod("Multiplier:NonVaalSummonedMinion", "BASE", output[activeSkill.minion.minionData.limit], "Config", { type = "Condition", var = "Combat" }) + -- Count active, damageable minions. Skills without a limit contribute one minion. + local minionList = activeSkill.minionList + if not activeSkill.skillFlags.disable and not activeSkill.skillTypes[SkillType.MinionsAreUndamagable] and minionList and minionList[1] then + local grantedEffect = activeSkill.activeEffect.grantedEffect + for _, minionType in ipairs(minionList) do + local minionData = env.data.minions[minionType] + if minionData and not minionData.hostile then + local key = minionData.limit or grantedEffect.id..":"..minionType + local count = 1 + if minionData.limit then + count = m_floor(modDB:Override(nil, minionData.limit) or (calcLib.val(activeSkill.skillModList, minionData.limit) * activeSkill.skillModList:More(activeSkill.skillCfg, "ActiveMinionLimit"))) + output[minionData.limit] = m_max(count, output[minionData.limit] or 0) + end + local counts = minionCounts[key] or { } + counts.total = m_max(count, counts.total or 0) + if not activeSkill.skillTypes[SkillType.Vaal] then + counts.nonVaal = m_max(count, counts.nonVaal or 0) + end + minionCounts[key] = counts end - minionCount[activeSkill.minion.minionData.limit] = true - t_insert(minionCount, activeSkill.minion.minionData.limit) end end if activeSkill.skillTypes[SkillType.CreatesMinion] and not activeSkill.skillTypes[SkillType.MinionsAreUndamagable] then @@ -1371,10 +1415,20 @@ function calcs.perform(env, skipEHP) end end - if #minionCount == 1 then - modDB.conditions["OnlyMinion"] = true + local summonedMinions = 0 + for _, counts in pairs(minionCounts) do + summonedMinions = summonedMinions + counts.total + modDB:NewMod("Multiplier:SummonedMinion", "BASE", counts.total, "Config", { type = "Condition", var = "Combat" }) + if counts.nonVaal then + modDB:NewMod("Multiplier:NonVaalSummonedMinion", "BASE", counts.nonVaal, "Config", { type = "Condition", var = "Combat" }) + end end + -- Companionship only works while exactly one Minion is summoned. + local summonedMinionOverride = env.configInput.multiplierSummonedMinion + summonedMinionOverride = summonedMinionOverride ~= 0 and summonedMinionOverride or nil + modDB.conditions["OnlyMinion"] = (summonedMinionOverride or summonedMinions) == 1 + -- Special Rarity / Quantity Calc for Bisco's local lootQuantityNormalEnemies = modDB:Sum("INC", nil, "LootQuantityNormalEnemies") output.LootQuantityNormalEnemies = (lootQuantityNormalEnemies > 0) and lootQuantityNormalEnemies + modDB:Sum("INC", nil, "LootQuantity") or 0 @@ -1820,11 +1874,7 @@ function calcs.perform(env, skipEHP) end end if not env.minion.hostile then - for _, value in ipairs(env.player.mainSkill.skillModList:List(env.player.mainSkill.skillCfg, "MinionModifier")) do - if not value.type or env.minion.type == value.type then - env.minion.modDB:AddMod(value.mod) - end - end + addMinionModifiers(env.player.mainSkill.skillModList, env.player.mainSkill.skillCfg, env.minion) for _, name in ipairs(env.minion.modDB:List(nil, "Keystone")) do if env.spec.tree.keystoneMap[name] then env.minion.modDB:AddList(env.spec.tree.keystoneMap[name].modList) @@ -3195,11 +3245,7 @@ function calcs.perform(env, skipEHP) modDB.multipliers["BuffOnSelf"] = (modDB.multipliers["BuffOnSelf"] or 0) + 1 end if env.minion then - for _, value in ipairs(modList:List(env.player.mainSkill.skillCfg, "MinionModifier")) do - if not value.type or env.minion.type == value.type then - env.minion.modDB:AddMod(value.mod) - end - end + addMinionModifiers(modList, env.player.mainSkill.skillCfg, env.minion) end end if env.minion then @@ -3497,6 +3543,130 @@ function calcs.perform(env, skipEHP) enemyDB:NewMod("DamageTaken", "INC", m_floor(enemyDB:Sum("INC", nil, "DamageTakenConsecratedGround") * effect), "Consecrated Ground") end + -- Full DPS and builds without a supported redirect do not need ally Life. + local needsAllyLife = not skipEHP and modDB:Sum("BASE", nil, unpack(allyLifeRedirects)) ~= 0 + env.player.allyLifeList = { } + if needsAllyLife then + local calculatedLifePool = { } + local spectreCount = 0 + local firstSpectre + local spectreLimit = m_min(output.ActiveSpectreLimit or 0, summonedMinionOverride or m_huge) + for _, activeSkill in ipairs(env.player.activeSkillList) do + local minion = activeSkill.minion + if minion and not activeSkill.skillFlags.disable and not activeSkill.skillTypes[SkillType.MinionsAreUndamagable] then + local skillPool = activeSkill.skillFlags.spectre and spectreLifePool or minionLifePoolBySkill[activeSkill.activeEffect.grantedEffect.name] + local pools = skillPool and { skillPool } or { } + + -- Companionship is stored on the supported skill, which identifies the one minion it affects. + local hasCompanionship + for _, buff in ipairs(activeSkill.buffList) do + for _, mod in ipairs(buff.modList) do + if mod.name == companionshipLifePool.redirect then + hasCompanionship = true + break + end + end + if hasCompanionship then + break + end + end + if hasCompanionship then + t_insert(pools, companionshipLifePool) + if skillPool and modDB:Sum("BASE", nil, skillPool.redirect) ~= 0 and modDB:Sum("BASE", nil, companionshipLifePool.redirect) ~= 0 then + -- Both redirects spend the same minion's Life, so they must share one pool. + pools = { companionshipLifePool } + modDB:NewMod("MinionLifeShares"..skillPool.life, "FLAG", true, "Companionship") + end + end + + local minionLife + for _, pool in ipairs(pools) do + local canAdd = ((pool == spectreLifePool and spectreCount < spectreLimit) + or (pool ~= spectreLifePool and not calculatedLifePool[pool.life])) + and modDB:Sum("BASE", nil, pool.redirect) ~= 0 and not modDB:Override(nil, pool.life) + if canAdd then + if not minionLife then + if minion ~= env.minion then + initMinionModDB(env, activeSkill) + addMinionModifiers(activeSkill.skillModList, activeSkill.skillCfg, minion) + for _, modList in pairs(buffs) do + addMinionModifiers(modList, activeSkill.skillCfg, minion) + end + for _, modList in pairs(minionBuffs) do + minion.modDB:AddList(modList) + end + for _, name in ipairs(minion.modDB:List(nil, "Keystone")) do + if env.spec.tree.keystoneMap[name] then + minion.modDB:AddList(env.spec.tree.keystoneMap[name].modList) + end + end + doActorAttribsConditions(env, minion) + else + doActorLifeMana(minion) + end + minionLife = minion.output.Life + end + + -- Void Spawn redirects scale from their summon limit, so their Life pool uses that same limit. + local count = pool.limit and (output[pool.limit] or calcLib.val(activeSkill.skillModList, pool.limit)) or 1 + if pool == spectreLifePool then + count = 1 + spectreCount = spectreCount + 1 + end + local life = minionLife * count + modDB:NewMod(pool.life, "BASE", life, pool.source) + env.player.allyLifeList[pool.life] = env.player.allyLifeList[pool.life] or { } + local entry = { name = minion.minionData.name, life = life, count = count } + t_insert(env.player.allyLifeList[pool.life], entry) + if pool == spectreLifePool then + firstSpectre = firstSpectre or { life = minionLife, entry = entry } + else + calculatedLifePool[pool.life] = true + end + end + end + end + end + if firstSpectre and spectreCount < spectreLimit then + -- Each Raise Spectre group represents one Spectre; the first group fills any slots left over. + local count = spectreLimit - spectreCount + local life = firstSpectre.life * count + firstSpectre.entry.count = firstSpectre.entry.count + count + firstSpectre.entry.life = firstSpectre.entry.life + life + modDB:NewMod(spectreLifePool.life, "BASE", life, spectreLifePool.source) + end + + local normalTotem + local vaalRejuvenationTotem + for _, activeSkill in ipairs(env.player.activeSkillList) do + if activeSkill.skillFlags.totem and not activeSkill.skillFlags.disable and activeSkill.skillData.totemLevel and activeSkill.skillTotemId then + local life = calcs.calcTotemLife(env, activeSkill) + local totem = { name = activeSkill.activeEffect.grantedEffect.name, life = life } + if totem.name == "Vaal Rejuvenation Totem" then + vaalRejuvenationTotem = totem + elseif not normalTotem or life > normalTotem.life then + normalTotem = totem + end + end + end + + -- PoB cannot know which Totem is nearest, so use the eligible type with the most Life. + local nearestTotem = vaalRejuvenationTotem and (not normalTotem or vaalRejuvenationTotem.life > normalTotem.life) and vaalRejuvenationTotem or normalTotem + local totemPool = totemLifePoolByStat.TotalTotemLife + local vaalPool = totemLifePoolByStat.TotalVaalRejuvenationTotemLife + local totemLifeOverride = modDB:Override(nil, "TotalTotemLife") + local totemRedirect = modDB:Sum("BASE", nil, totemPool.redirect) + local nearestIsVaal = nearestTotem and nearestTotem == vaalRejuvenationTotem and not totemLifeOverride + if nearestTotem and not nearestIsVaal and totemRedirect ~= 0 and not totemLifeOverride then + modDB:NewMod("TotalTotemLife", "BASE", nearestTotem.life, totemPool.source) + env.player.allyLifeList.TotalTotemLife = { nearestTotem } + end + if vaalRejuvenationTotem and (modDB:Sum("BASE", nil, vaalPool.redirect) ~= 0 or nearestIsVaal and totemRedirect ~= 0) and not modDB:Override(nil, "TotalVaalRejuvenationTotemLife") then + modDB:NewMod("TotalVaalRejuvenationTotemLife", "BASE", vaalRejuvenationTotem.life, vaalPool.source) + env.player.allyLifeList.TotalVaalRejuvenationTotemLife = { vaalRejuvenationTotem } + end + end + -- Defence/offence calculations calcs.defence(env, env.player) if not skipEHP then diff --git a/src/Modules/CalcSections.lua b/src/Modules/CalcSections.lua index 1597da27a94..0dc6da8a034 100644 --- a/src/Modules/CalcSections.lua +++ b/src/Modules/CalcSections.lua @@ -2179,6 +2179,12 @@ return { { modName = { "FrostGlobeHealth", "FrostGlobeDamageMitigation" } }, }, }, + { label = "Minion Ally", haveOutput = "TotalMinionLife", + { format = "{0:output:TotalMinionLife}", + { breakdown = "TotalMinionLife" }, + { modName = { "TotalMinionLife", "takenFromMinionBeforeYou" } }, + }, + }, { label = "Spectre Ally", haveOutput = "TotalSpectreLife", { format = "{0:output:TotalSpectreLife}", { breakdown = "TotalSpectreLife" }, @@ -2197,6 +2203,24 @@ return { { modName = { "TotalVaalRejuvenationTotemLife", "takenFromVaalRejuvenationTotemsBeforeYou", "takenFromTotemsBeforeYou" } }, }, }, + { label = "Sentinel of Radiance", haveOutput = "TotalRadianceSentinelLife", + { format = "{0:output:TotalRadianceSentinelLife}", + { breakdown = "TotalRadianceSentinelLife" }, + { modName = { "TotalRadianceSentinelLife", "takenFromRadianceSentinelBeforeYou" } }, + }, + }, + { label = "Void Spawn Ally", haveOutput = "TotalVoidSpawnLife", + { format = "{0:output:TotalVoidSpawnLife}", + { breakdown = "TotalVoidSpawnLife" }, + { modName = { "TotalVoidSpawnLife", "takenFromVoidSpawnBeforeYou" } }, + }, + }, + { label = "Stone Golem Ally", haveOutput = "TotalStoneGolemLife", + { format = "{0:output:TotalStoneGolemLife}", + { breakdown = "TotalStoneGolemLife" }, + { modName = { "TotalStoneGolemLife", "takenFromStoneGolemBeforeYou" } }, + }, + }, { label = "Soul Link", haveOutput = "AlliedEnergyShield", { format = "{0:output:AlliedEnergyShield}", { breakdown = "AlliedEnergyShield" }, diff --git a/src/Modules/ConfigOptions.lua b/src/Modules/ConfigOptions.lua index e77099db2db..afbdd1fe8c2 100644 --- a/src/Modules/ConfigOptions.lua +++ b/src/Modules/ConfigOptions.lua @@ -786,20 +786,26 @@ Huge sets the radius to 11. { var = "enemyRadius", type = "integer", label = "Enemy radius:", ifSkill = { "Seismic Trap", "Lightning Spire Trap", "Explosive Trap", "Molten Strike" }, includeTransfigured = true, tooltip = "Configure the radius of an enemy hitbox to calculate some area overlapping (shotgunning) effects.", apply = function(val, modList, enemyModList) modList:NewMod("EnemyRadius", "OVERRIDE", m_max(val, 1), "Config") end }, - { var = "TotalSpectreLife", type = "integer", label = "Total Spectre Life:", ifMod = "takenFromSpectresBeforeYou", ifSkill = "Raise Spectre", includeTransfigured = true, tooltip = "The total life of your Spectres that can be taken before yours (used by jinxed juju)", apply = function(val, modList, enemyModList) - modList:NewMod("TotalSpectreLife", "BASE", val, "Config") + { var = "TotalMinionLife", type = "integer", label = "Minion Life override:", ifMod = "takenFromMinionBeforeYou", tooltip = "Overrides the automatically calculated Life of the minion supported by Companionship.", apply = function(val, modList, enemyModList) + modList:NewMod("TotalMinionLife", "OVERRIDE", val, "Config") end }, - { var = "TotalTotemLife", type = "integer", label = "Total Totem Life:", ifOption = "conditionHaveTotem", ifMod = "takenFromTotemsBeforeYou", tooltip = "The total life of your Totems (excluding Vaal Rejuvenation Totem) that can be taken before yours (used by totem mastery)", apply = function(val, modList, enemyModList) - modList:NewMod("TotalTotemLife", "BASE", val, "Config") + { var = "TotalSpectreLife", type = "integer", label = "Total Spectre Life override:", ifMod = "takenFromSpectresBeforeYou", ifSkill = "Raise Spectre", includeTransfigured = true, tooltip = "Overrides the automatically calculated total Life of your Spectres that can be taken before yours (used by Jinxed Juju).", apply = function(val, modList, enemyModList) + modList:NewMod("TotalSpectreLife", "OVERRIDE", val, "Config") end }, - { var = "TotalRadianceSentinelLife", type = "integer", label = "Total life pool of Sentinel of Radiance", ifMod = "takenFromRadianceSentinelBeforeYou", apply = function(val, modList, enemyModList) - modList:NewMod("TotalRadianceSentinelLife", "BASE", val, "Config") + { var = "TotalTotemLife", type = "integer", label = "Nearest Totem Life override:", ifOption = "conditionHaveTotem", ifMod = "takenFromTotemsBeforeYou", tooltip = "Overrides the automatically calculated Life of your nearest Totem (excluding Vaal Rejuvenation Totem) that can be taken before yours (used by Totem Mastery).", apply = function(val, modList, enemyModList) + modList:NewMod("TotalTotemLife", "OVERRIDE", val, "Config") end }, - { var = "TotalVoidSpawnLife", type = "integer", label = "Total life pool of Void Spawn", ifMod = "takenFromVoidSpawnBeforeYou", apply = function(val, modList, enemyModList) - modList:NewMod("TotalVoidSpawnLife", "BASE", val, "Config") + { var = "TotalRadianceSentinelLife", type = "integer", label = "Sentinel of Radiance Life override:", ifMod = "takenFromRadianceSentinelBeforeYou", tooltip = "Overrides the automatically calculated Life of your Sentinel of Radiance that can be taken before yours.", apply = function(val, modList, enemyModList) + modList:NewMod("TotalRadianceSentinelLife", "OVERRIDE", val, "Config") end }, - { var = "TotalVaalRejuvenationTotemLife", type = "integer", label = "Total Vaal Rejuvenation Totem Life:", ifSkill = { "Vaal Rejuvenation Totem" }, ifMod = "takenFromVaalRejuvenationTotemsBeforeYou", tooltip = "The total life of your Vaal Rejuvenation Totems that can be taken before yours", apply = function(val, modList, enemyModList) - modList:NewMod("TotalVaalRejuvenationTotemLife", "BASE", val, "Config") + { var = "TotalVoidSpawnLife", type = "integer", label = "Total Void Spawn Life override:", ifMod = "takenFromVoidSpawnBeforeYou", tooltip = "Overrides the automatically calculated total Life of your Void Spawns that can be taken before yours.", apply = function(val, modList, enemyModList) + modList:NewMod("TotalVoidSpawnLife", "OVERRIDE", val, "Config") + end }, + { var = "TotalStoneGolemLife", type = "integer", label = "Stone Golem Life override:", ifSkill = "Summon Stone Golem of Safeguarding", ifMod = "takenFromStoneGolemBeforeYou", tooltip = "Overrides the automatically calculated Life of your Stone Golem of Safeguarding that can be taken before yours.", apply = function(val, modList, enemyModList) + modList:NewMod("TotalStoneGolemLife", "OVERRIDE", val, "Config") + end }, + { var = "TotalVaalRejuvenationTotemLife", type = "integer", label = "Vaal Rejuvenation Totem Life override:", ifSkill = "Vaal Rejuvenation Totem", ifMod = "takenFromVaalRejuvenationTotemsBeforeYou", tooltip = "Overrides the automatically calculated Life of your Vaal Rejuvenation Totem that can be taken before yours.", apply = function(val, modList, enemyModList) + modList:NewMod("TotalVaalRejuvenationTotemLife", "OVERRIDE", val, "Config") end }, { label = "^xAF6025Balance of Terror ^7Curse Disable:", ifCond = { "SelfCastConductivity", "SelfCastDespair", "SelfCastElementalWeakness", "SelfCastEnfeeble", "SelfCastFlammability", "SelfCastFrostbite", "SelfCastPunishment", "SelfCastTemporalChains", "SelfCastVulnerability" } }, { var = "balanceOfTerrorSelfCastConductivity", type = "check", label = "Conductivity self-only", ifSkill = "Conductivity", ifCond = "SelfCastConductivity", tooltip = "Counts Conductivity as self-cast for Balance of Terror without applying it to enemies." },