From 83e90e0a17194350207f09fbf462a21c8f24de1c Mon Sep 17 00:00:00 2001 From: ltogniolli Date: Tue, 30 Jun 2026 07:47:37 +0000 Subject: [PATCH 1/3] Apply changes from https://github.com/PathOfBuildingCommunity/PathOfBuilding-PoE2/pull/2159 --- spec/System/TestDefence_spec.lua.rej | 56 ++++++++++++++ src/Data/ModCache.lua.rej | 39 ++++++++++ src/Data/SkillStatMap.lua.rej | 10 +++ src/Modules/CalcActiveSkill.lua | 9 +++ src/Modules/CalcDefence.lua.rej | 39 ++++++++++ src/Modules/CalcPerform.lua | 112 +++++++++++++++++++++++++-- src/Modules/CalcPerform.lua.rej | 56 ++++++++++++++ src/Modules/CalcSections.lua | 12 ++- src/Modules/ConfigOptions.lua.rej | 24 ++++++ src/Modules/ModParser.lua.rej | 16 ++++ 10 files changed, 365 insertions(+), 8 deletions(-) create mode 100644 spec/System/TestDefence_spec.lua.rej create mode 100644 src/Data/ModCache.lua.rej create mode 100644 src/Data/SkillStatMap.lua.rej create mode 100644 src/Modules/CalcDefence.lua.rej create mode 100644 src/Modules/CalcPerform.lua.rej create mode 100644 src/Modules/ConfigOptions.lua.rej create mode 100644 src/Modules/ModParser.lua.rej diff --git a/spec/System/TestDefence_spec.lua.rej b/spec/System/TestDefence_spec.lua.rej new file mode 100644 index 00000000000..05e24ca7e26 --- /dev/null +++ b/spec/System/TestDefence_spec.lua.rej @@ -0,0 +1,56 @@ +diff a/spec/System/TestDefence_spec.lua b/spec/System/TestDefence_spec.lua (rejected hunks) +@@ -608,6 +608,30 @@ describe("TestDefence", function() + assert.is_true(block.TotalEHP > base.TotalEHP) + end) + ++ describe("damage taken from spectres' life before you", function() ++ it("sums spectre life automatically", function() ++ build.skillsTab:PasteSocketGroup("Spectre: Lightless Abomination 20/0 1") ++ build.configTab:BuildModList() ++ build.configTab.modList:NewMod("TakenFromSpectresBeforeYou", "BASE", 15, "Test") ++ build.calcsTab:BuildOutput() ++ ++ local output = build.calcsTab.mainOutput ++ assert.are.equals(15, output.SpectreAllyDamageMitigation) ++ local spectreLife = build.calcsTab.mainEnv.player.spectreLifeList[1].life ++ assert.are.equals(spectreLife, output.TotalSpectreLife) ++ end) ++ ++ it("uses configured spectre life as an override", function() ++ build.skillsTab:PasteSocketGroup("Spectre: Lightless Abomination 20/0 1") ++ 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) ++ end) ++ + describe("damage taken from companion's life before you", function() + it("redirects damage to the configured companion life pool", function() + build.configTab.input.enemyIsBoss = "None" +@@ -688,6 +712,23 @@ describe("TestDefence", function() + assert.are.near(mainSkillCompanionLife, output.TotalCompanionLife, 1) + end) + ++ it("stacks Loyalty's redirect from multiple companion skills", function() ++ build.skillsTab:PasteSocketGroup("Companion: Lightless Abomination 20/0 1\nLoyalty 1/0 1") ++ build.skillsTab:PasteSocketGroup("Companion: Lightless Moray 20/0 1\nLoyalty 1/0 1") ++ runCallback("OnFrame") ++ ++ assert.are.equals(20, build.calcsTab.calcsOutput.CompanionAllyDamageMitigation) ++ end) ++ ++ it("does not count global companion redirect once per companion skill", function() ++ build.configTab.input.customMods = "5% of Damage from Hits is taken from your Damageable Companion's Life before you" ++ build.skillsTab:PasteSocketGroup("Companion: Lightless Abomination 20/0 1") ++ build.skillsTab:PasteSocketGroup("Companion: Lightless Abomination 20/0 1") ++ pob1and2Compat() ++ ++ assert.are.equals(5, build.calcsTab.calcsOutput.CompanionAllyDamageMitigation) ++ end) ++ + it("has no effect with no companions and no config override", function() + build.configTab.input.enemyIsBoss = "None" + build.configTab.input.customMods = "" diff --git a/src/Data/ModCache.lua.rej b/src/Data/ModCache.lua.rej new file mode 100644 index 00000000000..c31e4801d8c --- /dev/null +++ b/src/Data/ModCache.lua.rej @@ -0,0 +1,39 @@ +diff a/src/Data/ModCache.lua b/src/Data/ModCache.lua (rejected hunks) +@@ -1541,7 +1541,7 @@ c["13% increased Skill Speed"]={{[1]={flags=0,keywordFlags=0,name="Speed",type=" + 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} + c["13% increased maximum Life"]={{[1]={flags=0,keywordFlags=0,name="Life",type="INC",value=13}},nil} + c["13% increased maximum Mana"]={{[1]={flags=0,keywordFlags=0,name="Mana",type="INC",value=13}},nil} +-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} ++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} + c["13% reduced Attack Speed"]={{[1]={flags=1,keywordFlags=0,name="Speed",type="INC",value=-13}},nil} + c["13% reduced Attack and Cast Speed"]={{[1]={flags=0,keywordFlags=0,name="Speed",type="INC",value=-13}},nil} + c["13% reduced Charges per use"]={{[1]={flags=0,keywordFlags=0,name="FlaskChargesUsed",type="INC",value=-13}},nil} +@@ -1740,8 +1740,8 @@ c["15% less maximum Mana"]={{[1]={flags=0,keywordFlags=0,name="Mana",type="MORE" + 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 "} + 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:"} + c["15% more Maximum Life"]={{[1]={flags=0,keywordFlags=0,name="Life",type="MORE",value=15}},nil} +-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} +-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} ++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} ++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} + c["15% of Damage is taken from Mana before Life"]={{[1]={flags=0,keywordFlags=0,name="DamageTakenFromManaBeforeLife",type="BASE",value=15}},nil} + c["15% of Damage taken Recouped as Life"]={{[1]={flags=0,keywordFlags=0,name="LifeRecoup",type="BASE",value=15}},nil} + 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 "} +@@ -2151,7 +2151,7 @@ c["20% more Damage against Heavy Stunned Enemies with Maces"]={{[1]={[1]={actor= + c["20% more Life Cost of Skills"]={{[1]={flags=0,keywordFlags=0,name="LifeCost",type="MORE",value=20}},nil} + 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} + c["20% of Cold Damage taken as Fire Damage"]={{[1]={flags=0,keywordFlags=0,name="ColdDamageTakenAsFire",type="BASE",value=20}},nil} +-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} ++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} + c["20% of Damage is taken from Mana before Life"]={{[1]={flags=0,keywordFlags=0,name="DamageTakenFromManaBeforeLife",type="BASE",value=20}},nil} + c["20% of Damage taken Recouped as Life"]={{[1]={flags=0,keywordFlags=0,name="LifeRecoup",type="BASE",value=20}},nil} + c["20% of Damage taken Recouped as Mana"]={{[1]={flags=0,keywordFlags=0,name="ManaRecoup",type="BASE",value=20}},nil} +@@ -3083,7 +3083,7 @@ c["5% increased Stun Threshold"]={{[1]={flags=0,keywordFlags=0,name="StunThresho + 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} + 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 "} + c["5% increased total Power counted by Warcries"]={{[1]={flags=0,keywordFlags=0,name="WarcryPower",type="INC",value=5}},nil} +-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} ++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} + c["5% of Damage taken Recouped as Life"]={{[1]={flags=0,keywordFlags=0,name="LifeRecoup",type="BASE",value=5}},nil} + 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} + c["5% of Maximum Life Converted to Energy Shield"]={{[1]={flags=0,keywordFlags=0,name="LifeConvertToEnergyShield",type="BASE",value=5}},nil} diff --git a/src/Data/SkillStatMap.lua.rej b/src/Data/SkillStatMap.lua.rej new file mode 100644 index 00000000000..62fa532a1c8 --- /dev/null +++ b/src/Data/SkillStatMap.lua.rej @@ -0,0 +1,10 @@ +diff a/src/Data/SkillStatMap.lua b/src/Data/SkillStatMap.lua (rejected hunks) +@@ -2528,7 +2528,7 @@ return { + mod("MinionModifier", "LIST", { mod = flag("Gigantic") }), + }, + ["companion_takes_%_damage_before_you_from_support"] = { +- mod("takenFromCompanionBeforeYou", "BASE", nil, 0, 0, { type = "GlobalEffect", effectType = "Buff", unscalable = true }), ++ mod("TakenFromCompanionBeforeYou", "BASE", nil, 0, 0, { type = "GlobalEffect", effectType = "Buff", unscalable = true }), + }, + ["minion_damage_+%_final_per_different_elemental_ailment_on_target"] = { + mod("MinionModifier", "LIST", { mod = mod("Damage", "MORE", nil, 0, 0, { type = "ActorCondition", actor = "enemy", var = "Electrocuted" }) }), diff --git a/src/Modules/CalcActiveSkill.lua b/src/Modules/CalcActiveSkill.lua index de735dc5a35..b7fec9d4d60 100644 --- a/src/Modules/CalcActiveSkill.lua +++ b/src/Modules/CalcActiveSkill.lua @@ -775,6 +775,15 @@ function calcs.buildActiveSkillModList(env, activeSkill) if tag.type == "GlobalEffect" then effectType = tag.effectType effectName = tag.effectName or activeGrantedEffect.name + if activeSkill.minion and activeSkill.minion.minionData then + if effectName:find("{0}", 1, true) then + effectName = effectName:gsub("{0}", activeSkill.minion.minionData.name) + elseif activeGrantedEffect.minionList and effectName:match("^Companion") then + effectName = "Companion: "..activeSkill.minion.minionData.name + elseif activeGrantedEffect.minionList and effectName:match("^Spectre") then + effectName = "Spectre: "..activeSkill.minion.minionData.name + end + end effectTag = tag break end diff --git a/src/Modules/CalcDefence.lua.rej b/src/Modules/CalcDefence.lua.rej new file mode 100644 index 00000000000..881c8f00d14 --- /dev/null +++ b/src/Modules/CalcDefence.lua.rej @@ -0,0 +1,39 @@ +diff a/src/Modules/CalcDefence.lua b/src/Modules/CalcDefence.lua (rejected hunks) +@@ -2924,13 +2924,23 @@ function calcs.buildDefenceEstimations(env, actor) + end + + -- from spectres +- output["SpectreAllyDamageMitigation"] = modDB:Sum("BASE", nil, "takenFromSpectresBeforeYou") ++ output["SpectreAllyDamageMitigation"] = modDB:Sum("BASE", nil, "TakenFromSpectresBeforeYou") + if output["SpectreAllyDamageMitigation"] ~= 0 then +- output["TotalSpectreLife"] = modDB:Sum("BASE", nil, "TotalSpectreLife") ++ output["TotalSpectreLife"] = modDB:Override(nil, "TotalSpectreLife") or modDB:Sum("BASE", nil, "TotalSpectreLife") ++ if breakdown then ++ breakdown["TotalSpectreLife"] = { } ++ if modDB:Override(nil, "TotalSpectreLife") then ++ t_insert(breakdown["TotalSpectreLife"], s_format("%d ^8(from config)", output["TotalSpectreLife"])) ++ else ++ for _, spectre in ipairs(actor.spectreLifeList or { }) do ++ t_insert(breakdown["TotalSpectreLife"], s_format("%d ^8(%s)", spectre.life, spectre.name)) ++ end ++ end ++ end + end + + -- from totems +- output["TotemAllyDamageMitigation"] = modDB:Sum("BASE", nil, "takenFromTotemsBeforeYou") ++ output["TotemAllyDamageMitigation"] = modDB:Sum("BASE", nil, "TakenFromTotemsBeforeYou") + if output["TotemAllyDamageMitigation"] ~= 0 then + output["TotalTotemLife"] = modDB:Sum("BASE", nil, "TotalTotemLife") + end +@@ -2948,8 +2958,8 @@ function calcs.buildDefenceEstimations(env, actor) + end + + -- from companions +- local companionMitigation = modDB:Sum("BASE", nil, "takenFromCompanionBeforeYou") +- local companionMitigationFromDeflected = modDB:Sum("BASE", nil, "takenFromCompanionBeforeYouFromDeflected") ++ local companionMitigation = modDB:Sum("BASE", nil, "TakenFromCompanionBeforeYou") ++ local companionMitigationFromDeflected = modDB:Sum("BASE", nil, "TakenFromCompanionBeforeYouFromDeflected") + output["CompanionAllyDamageMitigation"] = companionMitigation + companionMitigationFromDeflected * (output.DeflectChance or 0) / 100 + if output["CompanionAllyDamageMitigation"] ~= 0 then + output["TotalCompanionLife"] = modDB:Override(nil, "TotalCompanionLife") or modDB:Sum("BASE", nil, "TotalCompanionLife") diff --git a/src/Modules/CalcPerform.lua b/src/Modules/CalcPerform.lua index 03ab616ab83..1a80616602e 100644 --- a/src/Modules/CalcPerform.lua +++ b/src/Modules/CalcPerform.lua @@ -1083,6 +1083,112 @@ function calcs.actionSpeedMod(actor) return actionSpeedMod end +-- Initialises a minion's modifier database with its base stats (life, defences, resists), +-- monster type mods, tamed beast mods and player-granted mods, for the given owning skill +local function initMinionModDB(env, activeSkill) + local modDB = env.modDB + local minion = activeSkill.minion + 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 + minion.modDB:NewMod("LifeConvertToEnergyShield", "BASE", minion.minionData.energyShield * 100, "Base") + end + --Armour formula is math.floor((10 + 2 * level) * 1.067 ^ level) + minion.modDB:NewMod("Armour", "BASE", round(env.data.monsterArmourTable[minion.level] * (minion.minionData.armour or 1)), "Base") + --Evasion formula is math.floor((50 + 16 * level + 16 * level * (MonsterType.Evasion / 100)) * (1.0212 ^ level) + 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 + -- Minions no longer need Accuracy as of patch 0.3.0 + minion.modDB:NewMod("CannotBeEvaded", "FLAG", 1, "Minion Attacks always hit") + end + minion.modDB:NewMod("CritMultiplier", "BASE", env.data.monsterConstants["base_critical_hit_damage_bonus"] + env.data.playerMinionIntrinsicStats["base_critical_hit_damage_bonus"], "Base") + 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("ProjectileCount", "BASE", 1, "Base") + minion.modDB:NewMod("PhysicalHeavyStunBuildup", "MORE", data.monsterConstants["physical_hit_damage_stun_multiplier_+%_final_from_ot"], "Physical Damage") + minion.modDB:NewMod("EnemyHeavyStunBuildup", "MORE", data.monsterConstants["melee_hit_damage_stun_multiplier_+%_final_from_ot"], "Melee Damage", ModFlag.Melee) + minion.modDB:NewMod("Damage", "MORE", minion.hiddenDamageFixup * 100, "Hidden Level Scaling") + 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.talismanModList then + -- Adding mods provided by "Necromantic Talisman" + minion.modDB:AddList(env.talismanModList) + 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 + local quiverEffectMod = env.player.modDB:Sum("INC", nil, "EffectOfBonusesFromQuiver") / 100 + if quiverEffectMod > 0 then + for _, mod in ipairs(env.player.itemList["Weapon 2"].modList) do + local modCopy = copyTable(mod) + modCopy.source = "Many Sources:" .. tostring(quiverEffectMod * 100) .. "% Quiver Bonus Effect" + minion.modDB:ScaleAddMod(modCopy, quiverEffectMod) + end + end + end + 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:Flag(nil, "StrengthAddedToMinions") then + minion.modDB:NewMod("Str", "BASE", round(calcLib.val(modDB, "Str")), "Player") + end + if modDB:Flag(nil, "StrengthAddedToCompanions") and activeSkill.skillTypes[SkillType.Companion] then + minion.modDB:NewMod("Str", "BASE", round(calcLib.val(modDB, "Str")), "Sturdy Ally") + end + if modDB:Flag(nil, "HalfStrengthAddedToMinions") then + minion.modDB:NewMod("Str", "BASE", round(calcLib.val(modDB, "Str") * 0.5), "Player") + end + if modDB:Flag(nil, "DexterityAddedToMinions") then + minion.modDB:NewMod("Dex", "BASE", round(calcLib.val(modDB, "Dex")), "Dead can Dance") + end + if modDB:Flag(nil, "DexterityAddedToCompanions") and activeSkill.skillTypes[SkillType.Companion] then + minion.modDB:NewMod("Dex", "BASE", round(calcLib.val(modDB, "Dex")), "Tandem Assault") + end +end + +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 + -- Finalises the environment and performs the stat calculations: -- 1. Merges keystone modifiers -- 2. Initialises minion skills @@ -3195,11 +3301,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 diff --git a/src/Modules/CalcPerform.lua.rej b/src/Modules/CalcPerform.lua.rej new file mode 100644 index 00000000000..5ee80a6b33e --- /dev/null +++ b/src/Modules/CalcPerform.lua.rej @@ -0,0 +1,56 @@ +diff a/src/Modules/CalcPerform.lua b/src/Modules/CalcPerform.lua (rejected hunks) +@@ -3298,17 +3298,14 @@ function calcs.perform(env, skipEHP) + enemyDB:NewMod("DamageTaken", "INC", enemyDB:Sum("INC", nil, "DamageTakenConsecratedGround") * effect, "Consecrated Ground") + end + +- -- Total life of damageable companions, for "% of Damage from Hits is taken from your Companion's Life before you" +- if modDB:HasMod("BASE", nil, "takenFromCompanionBeforeYou", "takenFromCompanionBeforeYouFromDeflected") and not modDB:Override(nil, "TotalCompanionLife") then +- local totalCompanionLife = 0 +- local companionLifeList = { } ++ local function calcMinionLifePool(env, buffs, includeSkill) ++ local totalLife = 0 ++ local lifeList = { } + local seenMinions = { } + for _, activeSkill in ipairs(env.player.activeSkillList) do + local skillFlags = env.mode == "CALCS" and activeSkill.activeEffect.statSetCalcs.skillFlags or activeSkill.activeEffect.statSet.skillFlags + local minion = activeSkill.minion +- if minion and not seenMinions[minion] and not skillFlags.disable +- and activeSkill.skillTypes[SkillType.Companion] +- and not activeSkill.skillTypes[SkillType.MinionsAreUndamagable] then ++ if minion and not seenMinions[minion] and not skillFlags.disable and includeSkill(activeSkill, skillFlags) then + seenMinions[minion] = true + if minion ~= env.minion then + minion.output = minion.output or { } +@@ -3325,13 +3322,27 @@ function calcs.perform(env, skipEHP) + doActorAttribsConditions(env, minion) + end + calcs.doActorLifeManaSpirit(minion, true) +- totalCompanionLife = totalCompanionLife + minion.output.Life +- -- Companion gems share a single granted effect ("Companion: {0}") whose name is mutated +- -- globally for display, so derive the entry name from this skill's own minion +- local companionName = minion.minionData and minion.minionData.name or activeSkill.activeEffect.grantedEffect.name +- t_insert(companionLifeList, { name = companionName, life = minion.output.Life }) ++ totalLife = totalLife + minion.output.Life ++ t_insert(lifeList, { name = minion.minionData and minion.minionData.name or activeSkill.activeEffect.grantedEffect.name, life = minion.output.Life }) + end + end ++ return totalLife, lifeList ++ end ++ ++ -- Total life of spectres, for "% of Damage from Hits is taken from your Spectres' Life before you" ++ if modDB:HasMod("BASE", nil, "TakenFromSpectresBeforeYou") and not modDB:Override(nil, "TotalSpectreLife") then ++ local totalSpectreLife, spectreLifeList = calcMinionLifePool(env, buffs, function(activeSkill, skillFlags) ++ return skillFlags.spectre and not activeSkill.skillTypes[SkillType.MinionsAreUndamagable] ++ end) ++ modDB:NewMod("TotalSpectreLife", "BASE", totalSpectreLife, "Spectres") ++ env.player.spectreLifeList = spectreLifeList ++ end ++ ++ -- Total life of damageable companions, for "% of Damage from Hits is taken from your Companion's Life before you" ++ if modDB:HasMod("BASE", nil, "TakenFromCompanionBeforeYou", "TakenFromCompanionBeforeYouFromDeflected") and not modDB:Override(nil, "TotalCompanionLife") then ++ local totalCompanionLife, companionLifeList = calcMinionLifePool(env, buffs, function(activeSkill) ++ return activeSkill.skillTypes[SkillType.Companion] and not activeSkill.skillTypes[SkillType.MinionsAreUndamagable] ++ end) + modDB:NewMod("TotalCompanionLife", "BASE", totalCompanionLife, "Companions") + env.player.companionLifeList = companionLifeList + end diff --git a/src/Modules/CalcSections.lua b/src/Modules/CalcSections.lua index 7ed5469a942..0196db48185 100644 --- a/src/Modules/CalcSections.lua +++ b/src/Modules/CalcSections.lua @@ -2178,19 +2178,25 @@ return { { label = "Spectre Ally", haveOutput = "TotalSpectreLife", { format = "{0:output:TotalSpectreLife}", { breakdown = "TotalSpectreLife" }, - { modName = { "TotalSpectreLife", "takenFromSpectresBeforeYou" } }, + { modName = { "TotalSpectreLife", "TakenFromSpectresBeforeYou" } }, }, }, { label = "Totem Ally", haveOutput = "TotalTotemLife", { format = "{0:output:TotalTotemLife}", { breakdown = "TotalTotemLife" }, - { modName = { "TotalTotemLife", "takenFromTotemsBeforeYou" } }, + { modName = { "TotalTotemLife", "TakenFromTotemsBeforeYou" } }, }, }, { label = "Vaal Rejuv. Totem", haveOutput = "TotalVaalRejuvenationTotemLife", { format = "{0:output:TotalVaalRejuvenationTotemLife}", { breakdown = "TotalVaalRejuvenationTotemLife" }, - { modName = { "TotalVaalRejuvenationTotemLife", "takenFromVaalRejuvenationTotemsBeforeYou", "takenFromTotemsBeforeYou" } }, + { modName = { "TotalVaalRejuvenationTotemLife", "takenFromVaalRejuvenationTotemsBeforeYou", "TakenFromTotemsBeforeYou" } }, + }, + }, + { label = "Companion Ally", haveOutput = "TotalCompanionLife", + { format = "{0:output:TotalCompanionLife}", + { breakdown = "TotalCompanionLife" }, + { modName = { "TotalCompanionLife", "TakenFromCompanionBeforeYou", "TakenFromCompanionBeforeYouFromDeflected" } }, }, }, { label = "Soul Link", haveOutput = "AlliedEnergyShield", diff --git a/src/Modules/ConfigOptions.lua.rej b/src/Modules/ConfigOptions.lua.rej new file mode 100644 index 00000000000..f546c0924c3 --- /dev/null +++ b/src/Modules/ConfigOptions.lua.rej @@ -0,0 +1,24 @@ +diff a/src/Modules/ConfigOptions.lua b/src/Modules/ConfigOptions.lua (rejected hunks) +@@ -780,10 +780,10 @@ Huge sets the radius to 11. + { var = "enemyRadius", type = "countAllowZero", label = "Enemy radius:", ifSkill = { "Seismic Trap", "Lightning Spire Trap", "Explosive Trap" }, 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 = "countAllowZero", label = "Total Spectre Life:", ifMod = "takenFromSpectresBeforeYou", tooltip = "The total life of your Spectres that can be taken before yours", apply = function(val, modList, enemyModList) +- modList:NewMod("TotalSpectreLife", "BASE", val, "Config") ++ { var = "TotalSpectreLife", type = "countAllowZero", label = "Total Spectre Life override:", ifMod = "TakenFromSpectresBeforeYou", tooltip = "Overrides the automatically calculated total Life of your damageable Spectres that can be taken before yours", apply = function(val, modList, enemyModList) ++ modList:NewMod("TotalSpectreLife", "OVERRIDE", val, "Config") + end }, +- { var = "TotalTotemLife", type = "countAllowZero", 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) ++ { var = "TotalTotemLife", type = "countAllowZero", 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") + end }, + { var = "TotalRadianceSentinelLife", type = "countAllowZero", label = "Total life pool of Sentinel of Radiance", ifMod = "takenFromRadianceSentinelBeforeYou", apply = function(val, modList, enemyModList) +@@ -792,7 +792,7 @@ Huge sets the radius to 11. + { var = "TotalVaalRejuvenationTotemLife", type = "countAllowZero", 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") + end }, +- { var = "TotalCompanionLife", type = "countAllowZero", label = "Total Companion Life override:", ifMod = { "takenFromCompanionBeforeYou", "takenFromCompanionBeforeYouFromDeflected" }, tooltip = "Overrides the automatically calculated total life of your damageable Companions\nthat can be taken before yours (e.g. Starkonja's Head, Loyalty)", apply = function(val, modList, enemyModList) ++ { var = "TotalCompanionLife", type = "countAllowZero", label = "Total Companion Life override:", ifMod = { "TakenFromCompanionBeforeYou", "TakenFromCompanionBeforeYouFromDeflected" }, tooltip = "Overrides the automatically calculated total Life of your damageable Companions\nthat can be taken before yours (e.g. Starkonja's Head, Loyalty)", apply = function(val, modList, enemyModList) + modList:NewMod("TotalCompanionLife", "OVERRIDE", val, "Config") + end }, + -- Section: Combat options diff --git a/src/Modules/ModParser.lua.rej b/src/Modules/ModParser.lua.rej new file mode 100644 index 00000000000..4208aa1eae4 --- /dev/null +++ b/src/Modules/ModParser.lua.rej @@ -0,0 +1,16 @@ +diff a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua (rejected hunks) +@@ -5337,10 +5337,10 @@ local specialModList = { + ["(%d+)%% increased armour per second you've been stationary, up to a maximum of (%d+)%%"] = function(num, _, limit) return { + mod("Armour", "INC", num, { type = "Multiplier", var = "StationarySeconds", limit = tonumber(limit / num) }, { type = "Condition", var = "Stationary" }), + } end, +- ["(%d+)%% of damage from hits is taken from your spectres' life before you"] = function(num) return { mod("takenFromSpectresBeforeYou", "BASE", num) } end, +- ["(%d+)%% of damage from hits is taken from your nearest totem's life before you"] = function(num) return { mod("takenFromTotemsBeforeYou", "BASE", num, { type = "Condition", var = "HaveTotem" }) } end, +- ["(%d+)%% of damage from hits is taken from your damageable companion's life before you"] = function(num) return { mod("takenFromCompanionBeforeYou", "BASE", num) } end, +- ["(%d+)%% of damage from deflected hits is taken from damageable companion's life before you"] = function(num) return { mod("takenFromCompanionBeforeYouFromDeflected", "BASE", num) } end, ++ ["(%d+)%% of damage from hits is taken from your spectres' life before you"] = function(num) return { mod("TakenFromSpectresBeforeYou", "BASE", num) } end, ++ ["(%d+)%% of damage from hits is taken from your nearest totem's life before you"] = function(num) return { mod("TakenFromTotemsBeforeYou", "BASE", num, { type = "Condition", var = "HaveTotem" }) } end, ++ ["(%d+)%% of damage from hits is taken from your damageable companion's life before you"] = function(num) return { mod("TakenFromCompanionBeforeYou", "BASE", num) } end, ++ ["(%d+)%% of damage from deflected hits is taken from damageable companion's life before you"] = function(num) return { mod("TakenFromCompanionBeforeYouFromDeflected", "BASE", num) } end, + ["(%a+) resistance cannot be penetrated"] = function(_, res) return { flag("EnemyCannotPen"..(res:gsub("^%l", string.upper)).."Resistance") } end, + ["your base energy shield recharge delay is (%d+) seconds"] = function(num) return { mod("EnergyShieldRechargeBase", "OVERRIDE", num), } end, + -- Knockback From 7109bff5fae628e23716089d4aa316c4871d3022 Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Thu, 23 Jul 2026 10:51:04 +1000 Subject: [PATCH 2/3] Fix merge issues and compact code --- spec/System/TestDefence_spec.lua | 52 ++++++++ spec/System/TestDefence_spec.lua.rej | 56 -------- src/Data/ModCache.lua.rej | 39 ------ src/Data/SkillStatMap.lua | 3 + src/Data/SkillStatMap.lua.rej | 10 -- src/Modules/CalcActiveSkill.lua | 9 -- src/Modules/CalcDefence.lua | 192 ++++++++------------------- src/Modules/CalcDefence.lua.rej | 39 ------ src/Modules/CalcPerform.lua | 112 +--------------- src/Modules/CalcPerform.lua.rej | 56 -------- src/Modules/CalcSections.lua | 26 +++- src/Modules/ConfigOptions.lua | 3 + src/Modules/ConfigOptions.lua.rej | 24 ---- src/Modules/ModParser.lua.rej | 16 --- 14 files changed, 134 insertions(+), 503 deletions(-) delete mode 100644 spec/System/TestDefence_spec.lua.rej delete mode 100644 src/Data/ModCache.lua.rej delete mode 100644 src/Data/SkillStatMap.lua.rej delete mode 100644 src/Modules/CalcDefence.lua.rej delete mode 100644 src/Modules/CalcPerform.lua.rej delete mode 100644 src/Modules/ConfigOptions.lua.rej delete mode 100644 src/Modules/ModParser.lua.rej diff --git a/spec/System/TestDefence_spec.lua b/spec/System/TestDefence_spec.lua index d0c33555fdb..29753a271ac 100644 --- a/spec/System/TestDefence_spec.lua +++ b/spec/System/TestDefence_spec.lua @@ -616,6 +616,58 @@ describe("TestDefence", function() end) end) + describe("damage taken from allies' life before you", function() + for _, ally in ipairs({ + { 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("applies Stone Golem of Safeguarding to melee hits", function() + build.skillsTab:PasteSocketGroup("Summon Stone Golem of Safeguarding 20/0 1") + build.configTab.input.TotalStoneGolemLife = 1000 + build.configTab.input.enemyDamageType = "Melee" + build.configTab:BuildModList() + runCallback("OnFrame") + + local output = build.calcsTab.calcsOutput + assert.are.equals(15, output.StoneGolemAllyDamageMitigation) + assert.are.equals(1000, output.TotalStoneGolemLife) + local pools = build.calcsTab.calcs.reducePoolsByDamage(nil, { Physical = 100 }, build.calcsTab.calcsEnv.player) + assert.are.equals(985, 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.TotalStoneGolemLife = 1000 + 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) + end) + local function withinTenPercent(value, otherValue) local ratio = otherValue / value return 0.9 < ratio and ratio < 1.1 diff --git a/spec/System/TestDefence_spec.lua.rej b/spec/System/TestDefence_spec.lua.rej deleted file mode 100644 index 05e24ca7e26..00000000000 --- a/spec/System/TestDefence_spec.lua.rej +++ /dev/null @@ -1,56 +0,0 @@ -diff a/spec/System/TestDefence_spec.lua b/spec/System/TestDefence_spec.lua (rejected hunks) -@@ -608,6 +608,30 @@ describe("TestDefence", function() - assert.is_true(block.TotalEHP > base.TotalEHP) - end) - -+ describe("damage taken from spectres' life before you", function() -+ it("sums spectre life automatically", function() -+ build.skillsTab:PasteSocketGroup("Spectre: Lightless Abomination 20/0 1") -+ build.configTab:BuildModList() -+ build.configTab.modList:NewMod("TakenFromSpectresBeforeYou", "BASE", 15, "Test") -+ build.calcsTab:BuildOutput() -+ -+ local output = build.calcsTab.mainOutput -+ assert.are.equals(15, output.SpectreAllyDamageMitigation) -+ local spectreLife = build.calcsTab.mainEnv.player.spectreLifeList[1].life -+ assert.are.equals(spectreLife, output.TotalSpectreLife) -+ end) -+ -+ it("uses configured spectre life as an override", function() -+ build.skillsTab:PasteSocketGroup("Spectre: Lightless Abomination 20/0 1") -+ 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) -+ end) -+ - describe("damage taken from companion's life before you", function() - it("redirects damage to the configured companion life pool", function() - build.configTab.input.enemyIsBoss = "None" -@@ -688,6 +712,23 @@ describe("TestDefence", function() - assert.are.near(mainSkillCompanionLife, output.TotalCompanionLife, 1) - end) - -+ it("stacks Loyalty's redirect from multiple companion skills", function() -+ build.skillsTab:PasteSocketGroup("Companion: Lightless Abomination 20/0 1\nLoyalty 1/0 1") -+ build.skillsTab:PasteSocketGroup("Companion: Lightless Moray 20/0 1\nLoyalty 1/0 1") -+ runCallback("OnFrame") -+ -+ assert.are.equals(20, build.calcsTab.calcsOutput.CompanionAllyDamageMitigation) -+ end) -+ -+ it("does not count global companion redirect once per companion skill", function() -+ build.configTab.input.customMods = "5% of Damage from Hits is taken from your Damageable Companion's Life before you" -+ build.skillsTab:PasteSocketGroup("Companion: Lightless Abomination 20/0 1") -+ build.skillsTab:PasteSocketGroup("Companion: Lightless Abomination 20/0 1") -+ pob1and2Compat() -+ -+ assert.are.equals(5, build.calcsTab.calcsOutput.CompanionAllyDamageMitigation) -+ end) -+ - it("has no effect with no companions and no config override", function() - build.configTab.input.enemyIsBoss = "None" - build.configTab.input.customMods = "" diff --git a/src/Data/ModCache.lua.rej b/src/Data/ModCache.lua.rej deleted file mode 100644 index c31e4801d8c..00000000000 --- a/src/Data/ModCache.lua.rej +++ /dev/null @@ -1,39 +0,0 @@ -diff a/src/Data/ModCache.lua b/src/Data/ModCache.lua (rejected hunks) -@@ -1541,7 +1541,7 @@ c["13% increased Skill Speed"]={{[1]={flags=0,keywordFlags=0,name="Speed",type=" - 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} - c["13% increased maximum Life"]={{[1]={flags=0,keywordFlags=0,name="Life",type="INC",value=13}},nil} - c["13% increased maximum Mana"]={{[1]={flags=0,keywordFlags=0,name="Mana",type="INC",value=13}},nil} --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} -+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} - c["13% reduced Attack Speed"]={{[1]={flags=1,keywordFlags=0,name="Speed",type="INC",value=-13}},nil} - c["13% reduced Attack and Cast Speed"]={{[1]={flags=0,keywordFlags=0,name="Speed",type="INC",value=-13}},nil} - c["13% reduced Charges per use"]={{[1]={flags=0,keywordFlags=0,name="FlaskChargesUsed",type="INC",value=-13}},nil} -@@ -1740,8 +1740,8 @@ c["15% less maximum Mana"]={{[1]={flags=0,keywordFlags=0,name="Mana",type="MORE" - 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 "} - 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:"} - c["15% more Maximum Life"]={{[1]={flags=0,keywordFlags=0,name="Life",type="MORE",value=15}},nil} --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} --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} -+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} -+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} - c["15% of Damage is taken from Mana before Life"]={{[1]={flags=0,keywordFlags=0,name="DamageTakenFromManaBeforeLife",type="BASE",value=15}},nil} - c["15% of Damage taken Recouped as Life"]={{[1]={flags=0,keywordFlags=0,name="LifeRecoup",type="BASE",value=15}},nil} - 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 "} -@@ -2151,7 +2151,7 @@ c["20% more Damage against Heavy Stunned Enemies with Maces"]={{[1]={[1]={actor= - c["20% more Life Cost of Skills"]={{[1]={flags=0,keywordFlags=0,name="LifeCost",type="MORE",value=20}},nil} - 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} - c["20% of Cold Damage taken as Fire Damage"]={{[1]={flags=0,keywordFlags=0,name="ColdDamageTakenAsFire",type="BASE",value=20}},nil} --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} -+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} - c["20% of Damage is taken from Mana before Life"]={{[1]={flags=0,keywordFlags=0,name="DamageTakenFromManaBeforeLife",type="BASE",value=20}},nil} - c["20% of Damage taken Recouped as Life"]={{[1]={flags=0,keywordFlags=0,name="LifeRecoup",type="BASE",value=20}},nil} - c["20% of Damage taken Recouped as Mana"]={{[1]={flags=0,keywordFlags=0,name="ManaRecoup",type="BASE",value=20}},nil} -@@ -3083,7 +3083,7 @@ c["5% increased Stun Threshold"]={{[1]={flags=0,keywordFlags=0,name="StunThresho - 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} - 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 "} - c["5% increased total Power counted by Warcries"]={{[1]={flags=0,keywordFlags=0,name="WarcryPower",type="INC",value=5}},nil} --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} -+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} - c["5% of Damage taken Recouped as Life"]={{[1]={flags=0,keywordFlags=0,name="LifeRecoup",type="BASE",value=5}},nil} - 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} - c["5% of Maximum Life Converted to Energy Shield"]={{[1]={flags=0,keywordFlags=0,name="LifeConvertToEnergyShield",type="BASE",value=5}},nil} 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/SkillStatMap.lua.rej b/src/Data/SkillStatMap.lua.rej deleted file mode 100644 index 62fa532a1c8..00000000000 --- a/src/Data/SkillStatMap.lua.rej +++ /dev/null @@ -1,10 +0,0 @@ -diff a/src/Data/SkillStatMap.lua b/src/Data/SkillStatMap.lua (rejected hunks) -@@ -2528,7 +2528,7 @@ return { - mod("MinionModifier", "LIST", { mod = flag("Gigantic") }), - }, - ["companion_takes_%_damage_before_you_from_support"] = { -- mod("takenFromCompanionBeforeYou", "BASE", nil, 0, 0, { type = "GlobalEffect", effectType = "Buff", unscalable = true }), -+ mod("TakenFromCompanionBeforeYou", "BASE", nil, 0, 0, { type = "GlobalEffect", effectType = "Buff", unscalable = true }), - }, - ["minion_damage_+%_final_per_different_elemental_ailment_on_target"] = { - mod("MinionModifier", "LIST", { mod = mod("Damage", "MORE", nil, 0, 0, { type = "ActorCondition", actor = "enemy", var = "Electrocuted" }) }), diff --git a/src/Modules/CalcActiveSkill.lua b/src/Modules/CalcActiveSkill.lua index b7fec9d4d60..de735dc5a35 100644 --- a/src/Modules/CalcActiveSkill.lua +++ b/src/Modules/CalcActiveSkill.lua @@ -775,15 +775,6 @@ function calcs.buildActiveSkillModList(env, activeSkill) if tag.type == "GlobalEffect" then effectType = tag.effectType effectName = tag.effectName or activeGrantedEffect.name - if activeSkill.minion and activeSkill.minion.minionData then - if effectName:find("{0}", 1, true) then - effectName = effectName:gsub("{0}", activeSkill.minion.minionData.name) - elseif activeGrantedEffect.minionList and effectName:match("^Companion") then - effectName = "Companion: "..activeSkill.minion.minionData.name - elseif activeGrantedEffect.minionList and effectName:match("^Spectre") then - effectName = "Spectre: "..activeSkill.minion.minionData.name - end - end effectTag = tag break end diff --git a/src/Modules/CalcDefence.lua b/src/Modules/CalcDefence.lua index 2362948bc52..4040362925c 100644 --- a/src/Modules/CalcDefence.lua +++ b/src/Modules/CalcDefence.lua @@ -28,6 +28,30 @@ local dmgTypeList = {"Physical", "Lightning", "Cold", "Fire", "Chaos"} local resistTypeList = { "Fire", "Cold", "Lightning", "Chaos" } +local allyLifePoolList = { + { key = "frostShield", life = "FrostShieldLife", mitigation = "FrostShieldDamageMitigation", label = "Frost Shield Life" }, + { key = "minion", life = "TotalMinionLife", mitigation = "MinionAllyDamageMitigation", label = "Total Minion Life" }, + { key = "spectres", life = "TotalSpectreLife", mitigation = "SpectreAllyDamageMitigation", label = "Total Spectre Life" }, + { key = "totems", life = "TotalTotemLife", mitigation = "TotemAllyDamageMitigation", label = "Total Totem Life" }, + { key = "vaalRejuvenationTotems", life = "TotalVaalRejuvenationTotemLife", mitigation = "VaalRejuvenationTotemAllyDamageMitigation", label = "Total Vaal Rejuvenation Totem Life" }, + { key = "radianceSentinel", life = "TotalRadianceSentinelLife", mitigation = "RadianceSentinelAllyDamageMitigation", label = "Total Sentinel of Radiance Life" }, + { key = "voidSpawn", life = "TotalVoidSpawnLife", mitigation = "VoidSpawnAllyDamageMitigation", label = "Total Void Spawn Life" }, + { key = "stoneGolem", life = "TotalStoneGolemLife", mitigation = "StoneGolemAllyDamageMitigation", label = "Total Stone Golem Life" }, + { key = "soulLink", life = "AlliedEnergyShield", mitigation = "SoulLinkMitigation", label = "Total Allied Energy shield" }, +} + +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 = mitigation / 100 } + end + end + return pools +end + -- Calculate hit chance function calcs.hitChance(evasion, accuracy) if accuracy < 0 then @@ -173,31 +197,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 { } @@ -425,29 +425,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 @@ -2501,6 +2484,13 @@ function calcs.buildDefenceEstimations(env, actor) if output["VoidSpawnAllyDamageMitigation"] ~= 0 then output["TotalVoidSpawnLife"] = modDB:Sum("BASE", nil, "TotalVoidSpawnLife") end + + -- from Stone Golem of Safeguarding + local stoneGolemMitigation = modDB:Sum("BASE", nil, "takenFromStoneGolemBeforeYou") + output["StoneGolemAllyDamageMitigation"] = damageCategoryConfig == "Melee" and stoneGolemMitigation or damageCategoryConfig == "Average" and stoneGolemMitigation / 4 or 0 + if output["StoneGolemAllyDamageMitigation"] ~= 0 then + output["TotalStoneGolemLife"] = modDB:Sum("BASE", nil, "TotalStoneGolemLife") + end -- from Allied Energy Shield output["SoulLinkMitigation"] = modDB:Sum("BASE", nil, "TakenFromParentESBeforeYou") @@ -2588,31 +2578,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, @@ -3157,42 +3123,13 @@ function calcs.buildDefenceEstimations(env, actor) 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) + 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 + 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 @@ -3804,37 +3741,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/CalcDefence.lua.rej b/src/Modules/CalcDefence.lua.rej deleted file mode 100644 index 881c8f00d14..00000000000 --- a/src/Modules/CalcDefence.lua.rej +++ /dev/null @@ -1,39 +0,0 @@ -diff a/src/Modules/CalcDefence.lua b/src/Modules/CalcDefence.lua (rejected hunks) -@@ -2924,13 +2924,23 @@ function calcs.buildDefenceEstimations(env, actor) - end - - -- from spectres -- output["SpectreAllyDamageMitigation"] = modDB:Sum("BASE", nil, "takenFromSpectresBeforeYou") -+ output["SpectreAllyDamageMitigation"] = modDB:Sum("BASE", nil, "TakenFromSpectresBeforeYou") - if output["SpectreAllyDamageMitigation"] ~= 0 then -- output["TotalSpectreLife"] = modDB:Sum("BASE", nil, "TotalSpectreLife") -+ output["TotalSpectreLife"] = modDB:Override(nil, "TotalSpectreLife") or modDB:Sum("BASE", nil, "TotalSpectreLife") -+ if breakdown then -+ breakdown["TotalSpectreLife"] = { } -+ if modDB:Override(nil, "TotalSpectreLife") then -+ t_insert(breakdown["TotalSpectreLife"], s_format("%d ^8(from config)", output["TotalSpectreLife"])) -+ else -+ for _, spectre in ipairs(actor.spectreLifeList or { }) do -+ t_insert(breakdown["TotalSpectreLife"], s_format("%d ^8(%s)", spectre.life, spectre.name)) -+ end -+ end -+ end - end - - -- from totems -- output["TotemAllyDamageMitigation"] = modDB:Sum("BASE", nil, "takenFromTotemsBeforeYou") -+ output["TotemAllyDamageMitigation"] = modDB:Sum("BASE", nil, "TakenFromTotemsBeforeYou") - if output["TotemAllyDamageMitigation"] ~= 0 then - output["TotalTotemLife"] = modDB:Sum("BASE", nil, "TotalTotemLife") - end -@@ -2948,8 +2958,8 @@ function calcs.buildDefenceEstimations(env, actor) - end - - -- from companions -- local companionMitigation = modDB:Sum("BASE", nil, "takenFromCompanionBeforeYou") -- local companionMitigationFromDeflected = modDB:Sum("BASE", nil, "takenFromCompanionBeforeYouFromDeflected") -+ local companionMitigation = modDB:Sum("BASE", nil, "TakenFromCompanionBeforeYou") -+ local companionMitigationFromDeflected = modDB:Sum("BASE", nil, "TakenFromCompanionBeforeYouFromDeflected") - output["CompanionAllyDamageMitigation"] = companionMitigation + companionMitigationFromDeflected * (output.DeflectChance or 0) / 100 - if output["CompanionAllyDamageMitigation"] ~= 0 then - output["TotalCompanionLife"] = modDB:Override(nil, "TotalCompanionLife") or modDB:Sum("BASE", nil, "TotalCompanionLife") diff --git a/src/Modules/CalcPerform.lua b/src/Modules/CalcPerform.lua index 26cf0ba51f0..1fb88582c4c 100644 --- a/src/Modules/CalcPerform.lua +++ b/src/Modules/CalcPerform.lua @@ -1083,112 +1083,6 @@ function calcs.actionSpeedMod(actor) return actionSpeedMod end --- Initialises a minion's modifier database with its base stats (life, defences, resists), --- monster type mods, tamed beast mods and player-granted mods, for the given owning skill -local function initMinionModDB(env, activeSkill) - local modDB = env.modDB - local minion = activeSkill.minion - 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 - minion.modDB:NewMod("LifeConvertToEnergyShield", "BASE", minion.minionData.energyShield * 100, "Base") - end - --Armour formula is math.floor((10 + 2 * level) * 1.067 ^ level) - minion.modDB:NewMod("Armour", "BASE", round(env.data.monsterArmourTable[minion.level] * (minion.minionData.armour or 1)), "Base") - --Evasion formula is math.floor((50 + 16 * level + 16 * level * (MonsterType.Evasion / 100)) * (1.0212 ^ level) - 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 - -- Minions no longer need Accuracy as of patch 0.3.0 - minion.modDB:NewMod("CannotBeEvaded", "FLAG", 1, "Minion Attacks always hit") - end - minion.modDB:NewMod("CritMultiplier", "BASE", env.data.monsterConstants["base_critical_hit_damage_bonus"] + env.data.playerMinionIntrinsicStats["base_critical_hit_damage_bonus"], "Base") - 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("ProjectileCount", "BASE", 1, "Base") - minion.modDB:NewMod("PhysicalHeavyStunBuildup", "MORE", data.monsterConstants["physical_hit_damage_stun_multiplier_+%_final_from_ot"], "Physical Damage") - minion.modDB:NewMod("EnemyHeavyStunBuildup", "MORE", data.monsterConstants["melee_hit_damage_stun_multiplier_+%_final_from_ot"], "Melee Damage", ModFlag.Melee) - minion.modDB:NewMod("Damage", "MORE", minion.hiddenDamageFixup * 100, "Hidden Level Scaling") - 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.talismanModList then - -- Adding mods provided by "Necromantic Talisman" - minion.modDB:AddList(env.talismanModList) - 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 - local quiverEffectMod = env.player.modDB:Sum("INC", nil, "EffectOfBonusesFromQuiver") / 100 - if quiverEffectMod > 0 then - for _, mod in ipairs(env.player.itemList["Weapon 2"].modList) do - local modCopy = copyTable(mod) - modCopy.source = "Many Sources:" .. tostring(quiverEffectMod * 100) .. "% Quiver Bonus Effect" - minion.modDB:ScaleAddMod(modCopy, quiverEffectMod) - end - end - end - 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:Flag(nil, "StrengthAddedToMinions") then - minion.modDB:NewMod("Str", "BASE", round(calcLib.val(modDB, "Str")), "Player") - end - if modDB:Flag(nil, "StrengthAddedToCompanions") and activeSkill.skillTypes[SkillType.Companion] then - minion.modDB:NewMod("Str", "BASE", round(calcLib.val(modDB, "Str")), "Sturdy Ally") - end - if modDB:Flag(nil, "HalfStrengthAddedToMinions") then - minion.modDB:NewMod("Str", "BASE", round(calcLib.val(modDB, "Str") * 0.5), "Player") - end - if modDB:Flag(nil, "DexterityAddedToMinions") then - minion.modDB:NewMod("Dex", "BASE", round(calcLib.val(modDB, "Dex")), "Dead can Dance") - end - if modDB:Flag(nil, "DexterityAddedToCompanions") and activeSkill.skillTypes[SkillType.Companion] then - minion.modDB:NewMod("Dex", "BASE", round(calcLib.val(modDB, "Dex")), "Tandem Assault") - end -end - -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 - -- Finalises the environment and performs the stat calculations: -- 1. Merges keystone modifiers -- 2. Initialises minion skills @@ -3301,7 +3195,11 @@ function calcs.perform(env, skipEHP) modDB.multipliers["BuffOnSelf"] = (modDB.multipliers["BuffOnSelf"] or 0) + 1 end if env.minion then - addMinionModifiers(modList, env.player.mainSkill.skillCfg, env.minion) + 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 end end if env.minion then diff --git a/src/Modules/CalcPerform.lua.rej b/src/Modules/CalcPerform.lua.rej deleted file mode 100644 index 5ee80a6b33e..00000000000 --- a/src/Modules/CalcPerform.lua.rej +++ /dev/null @@ -1,56 +0,0 @@ -diff a/src/Modules/CalcPerform.lua b/src/Modules/CalcPerform.lua (rejected hunks) -@@ -3298,17 +3298,14 @@ function calcs.perform(env, skipEHP) - enemyDB:NewMod("DamageTaken", "INC", enemyDB:Sum("INC", nil, "DamageTakenConsecratedGround") * effect, "Consecrated Ground") - end - -- -- Total life of damageable companions, for "% of Damage from Hits is taken from your Companion's Life before you" -- if modDB:HasMod("BASE", nil, "takenFromCompanionBeforeYou", "takenFromCompanionBeforeYouFromDeflected") and not modDB:Override(nil, "TotalCompanionLife") then -- local totalCompanionLife = 0 -- local companionLifeList = { } -+ local function calcMinionLifePool(env, buffs, includeSkill) -+ local totalLife = 0 -+ local lifeList = { } - local seenMinions = { } - for _, activeSkill in ipairs(env.player.activeSkillList) do - local skillFlags = env.mode == "CALCS" and activeSkill.activeEffect.statSetCalcs.skillFlags or activeSkill.activeEffect.statSet.skillFlags - local minion = activeSkill.minion -- if minion and not seenMinions[minion] and not skillFlags.disable -- and activeSkill.skillTypes[SkillType.Companion] -- and not activeSkill.skillTypes[SkillType.MinionsAreUndamagable] then -+ if minion and not seenMinions[minion] and not skillFlags.disable and includeSkill(activeSkill, skillFlags) then - seenMinions[minion] = true - if minion ~= env.minion then - minion.output = minion.output or { } -@@ -3325,13 +3322,27 @@ function calcs.perform(env, skipEHP) - doActorAttribsConditions(env, minion) - end - calcs.doActorLifeManaSpirit(minion, true) -- totalCompanionLife = totalCompanionLife + minion.output.Life -- -- Companion gems share a single granted effect ("Companion: {0}") whose name is mutated -- -- globally for display, so derive the entry name from this skill's own minion -- local companionName = minion.minionData and minion.minionData.name or activeSkill.activeEffect.grantedEffect.name -- t_insert(companionLifeList, { name = companionName, life = minion.output.Life }) -+ totalLife = totalLife + minion.output.Life -+ t_insert(lifeList, { name = minion.minionData and minion.minionData.name or activeSkill.activeEffect.grantedEffect.name, life = minion.output.Life }) - end - end -+ return totalLife, lifeList -+ end -+ -+ -- Total life of spectres, for "% of Damage from Hits is taken from your Spectres' Life before you" -+ if modDB:HasMod("BASE", nil, "TakenFromSpectresBeforeYou") and not modDB:Override(nil, "TotalSpectreLife") then -+ local totalSpectreLife, spectreLifeList = calcMinionLifePool(env, buffs, function(activeSkill, skillFlags) -+ return skillFlags.spectre and not activeSkill.skillTypes[SkillType.MinionsAreUndamagable] -+ end) -+ modDB:NewMod("TotalSpectreLife", "BASE", totalSpectreLife, "Spectres") -+ env.player.spectreLifeList = spectreLifeList -+ end -+ -+ -- Total life of damageable companions, for "% of Damage from Hits is taken from your Companion's Life before you" -+ if modDB:HasMod("BASE", nil, "TakenFromCompanionBeforeYou", "TakenFromCompanionBeforeYouFromDeflected") and not modDB:Override(nil, "TotalCompanionLife") then -+ local totalCompanionLife, companionLifeList = calcMinionLifePool(env, buffs, function(activeSkill) -+ return activeSkill.skillTypes[SkillType.Companion] and not activeSkill.skillTypes[SkillType.MinionsAreUndamagable] -+ end) - modDB:NewMod("TotalCompanionLife", "BASE", totalCompanionLife, "Companions") - env.player.companionLifeList = companionLifeList - end diff --git a/src/Modules/CalcSections.lua b/src/Modules/CalcSections.lua index bcc89d65aab..067556fd467 100644 --- a/src/Modules/CalcSections.lua +++ b/src/Modules/CalcSections.lua @@ -2182,25 +2182,37 @@ return { { label = "Spectre Ally", haveOutput = "TotalSpectreLife", { format = "{0:output:TotalSpectreLife}", { breakdown = "TotalSpectreLife" }, - { modName = { "TotalSpectreLife", "TakenFromSpectresBeforeYou" } }, + { modName = { "TotalSpectreLife", "takenFromSpectresBeforeYou" } }, }, }, { label = "Totem Ally", haveOutput = "TotalTotemLife", { format = "{0:output:TotalTotemLife}", { breakdown = "TotalTotemLife" }, - { modName = { "TotalTotemLife", "TakenFromTotemsBeforeYou" } }, + { modName = { "TotalTotemLife", "takenFromTotemsBeforeYou" } }, }, }, { label = "Vaal Rejuv. Totem", haveOutput = "TotalVaalRejuvenationTotemLife", { format = "{0:output:TotalVaalRejuvenationTotemLife}", { breakdown = "TotalVaalRejuvenationTotemLife" }, - { modName = { "TotalVaalRejuvenationTotemLife", "takenFromVaalRejuvenationTotemsBeforeYou", "TakenFromTotemsBeforeYou" } }, + { modName = { "TotalVaalRejuvenationTotemLife", "takenFromVaalRejuvenationTotemsBeforeYou", "takenFromTotemsBeforeYou" } }, }, }, - { label = "Companion Ally", haveOutput = "TotalCompanionLife", - { format = "{0:output:TotalCompanionLife}", - { breakdown = "TotalCompanionLife" }, - { modName = { "TotalCompanionLife", "TakenFromCompanionBeforeYou", "TakenFromCompanionBeforeYouFromDeflected" } }, + { 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", diff --git a/src/Modules/ConfigOptions.lua b/src/Modules/ConfigOptions.lua index e77099db2db..89b35eb3a54 100644 --- a/src/Modules/ConfigOptions.lua +++ b/src/Modules/ConfigOptions.lua @@ -798,6 +798,9 @@ Huge sets the radius to 11. { var = "TotalVoidSpawnLife", type = "integer", label = "Total life pool of Void Spawn", ifMod = "takenFromVoidSpawnBeforeYou", apply = function(val, modList, enemyModList) modList:NewMod("TotalVoidSpawnLife", "BASE", val, "Config") end }, + { var = "TotalStoneGolemLife", type = "integer", label = "Total Stone Golem Life:", ifSkill = "Summon Stone Golem of Safeguarding", ifMod = "takenFromStoneGolemBeforeYou", tooltip = "The total life of your Stone Golems of Safeguarding that can be taken before yours", apply = function(val, modList, enemyModList) + modList:NewMod("TotalStoneGolemLife", "BASE", 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") end }, diff --git a/src/Modules/ConfigOptions.lua.rej b/src/Modules/ConfigOptions.lua.rej deleted file mode 100644 index f546c0924c3..00000000000 --- a/src/Modules/ConfigOptions.lua.rej +++ /dev/null @@ -1,24 +0,0 @@ -diff a/src/Modules/ConfigOptions.lua b/src/Modules/ConfigOptions.lua (rejected hunks) -@@ -780,10 +780,10 @@ Huge sets the radius to 11. - { var = "enemyRadius", type = "countAllowZero", label = "Enemy radius:", ifSkill = { "Seismic Trap", "Lightning Spire Trap", "Explosive Trap" }, 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 = "countAllowZero", label = "Total Spectre Life:", ifMod = "takenFromSpectresBeforeYou", tooltip = "The total life of your Spectres that can be taken before yours", apply = function(val, modList, enemyModList) -- modList:NewMod("TotalSpectreLife", "BASE", val, "Config") -+ { var = "TotalSpectreLife", type = "countAllowZero", label = "Total Spectre Life override:", ifMod = "TakenFromSpectresBeforeYou", tooltip = "Overrides the automatically calculated total Life of your damageable Spectres that can be taken before yours", apply = function(val, modList, enemyModList) -+ modList:NewMod("TotalSpectreLife", "OVERRIDE", val, "Config") - end }, -- { var = "TotalTotemLife", type = "countAllowZero", 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) -+ { var = "TotalTotemLife", type = "countAllowZero", 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") - end }, - { var = "TotalRadianceSentinelLife", type = "countAllowZero", label = "Total life pool of Sentinel of Radiance", ifMod = "takenFromRadianceSentinelBeforeYou", apply = function(val, modList, enemyModList) -@@ -792,7 +792,7 @@ Huge sets the radius to 11. - { var = "TotalVaalRejuvenationTotemLife", type = "countAllowZero", 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") - end }, -- { var = "TotalCompanionLife", type = "countAllowZero", label = "Total Companion Life override:", ifMod = { "takenFromCompanionBeforeYou", "takenFromCompanionBeforeYouFromDeflected" }, tooltip = "Overrides the automatically calculated total life of your damageable Companions\nthat can be taken before yours (e.g. Starkonja's Head, Loyalty)", apply = function(val, modList, enemyModList) -+ { var = "TotalCompanionLife", type = "countAllowZero", label = "Total Companion Life override:", ifMod = { "TakenFromCompanionBeforeYou", "TakenFromCompanionBeforeYouFromDeflected" }, tooltip = "Overrides the automatically calculated total Life of your damageable Companions\nthat can be taken before yours (e.g. Starkonja's Head, Loyalty)", apply = function(val, modList, enemyModList) - modList:NewMod("TotalCompanionLife", "OVERRIDE", val, "Config") - end }, - -- Section: Combat options diff --git a/src/Modules/ModParser.lua.rej b/src/Modules/ModParser.lua.rej deleted file mode 100644 index 4208aa1eae4..00000000000 --- a/src/Modules/ModParser.lua.rej +++ /dev/null @@ -1,16 +0,0 @@ -diff a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua (rejected hunks) -@@ -5337,10 +5337,10 @@ local specialModList = { - ["(%d+)%% increased armour per second you've been stationary, up to a maximum of (%d+)%%"] = function(num, _, limit) return { - mod("Armour", "INC", num, { type = "Multiplier", var = "StationarySeconds", limit = tonumber(limit / num) }, { type = "Condition", var = "Stationary" }), - } end, -- ["(%d+)%% of damage from hits is taken from your spectres' life before you"] = function(num) return { mod("takenFromSpectresBeforeYou", "BASE", num) } end, -- ["(%d+)%% of damage from hits is taken from your nearest totem's life before you"] = function(num) return { mod("takenFromTotemsBeforeYou", "BASE", num, { type = "Condition", var = "HaveTotem" }) } end, -- ["(%d+)%% of damage from hits is taken from your damageable companion's life before you"] = function(num) return { mod("takenFromCompanionBeforeYou", "BASE", num) } end, -- ["(%d+)%% of damage from deflected hits is taken from damageable companion's life before you"] = function(num) return { mod("takenFromCompanionBeforeYouFromDeflected", "BASE", num) } end, -+ ["(%d+)%% of damage from hits is taken from your spectres' life before you"] = function(num) return { mod("TakenFromSpectresBeforeYou", "BASE", num) } end, -+ ["(%d+)%% of damage from hits is taken from your nearest totem's life before you"] = function(num) return { mod("TakenFromTotemsBeforeYou", "BASE", num, { type = "Condition", var = "HaveTotem" }) } end, -+ ["(%d+)%% of damage from hits is taken from your damageable companion's life before you"] = function(num) return { mod("TakenFromCompanionBeforeYou", "BASE", num) } end, -+ ["(%d+)%% of damage from deflected hits is taken from damageable companion's life before you"] = function(num) return { mod("TakenFromCompanionBeforeYouFromDeflected", "BASE", num) } end, - ["(%a+) resistance cannot be penetrated"] = function(_, res) return { flag("EnemyCannotPen"..(res:gsub("^%l", string.upper)).."Resistance") } end, - ["your base energy shield recharge delay is (%d+) seconds"] = function(num) return { mod("EnergyShieldRechargeBase", "OVERRIDE", num), } end, - -- Knockback From b1b104686193b9035442cdaee24aa9cbd7728faa Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Fri, 24 Jul 2026 01:40:55 +1000 Subject: [PATCH 3/3] Automatically calcs life pools Automatically calcs life values for the different minion types Also compacts the defence code into loops so it's easier to add more mods in the future --- spec/System/TestDefence_spec.lua | 280 +++++++++++++++++++++- src/Data/Skills/sup_dex.lua | 2 +- src/Export/Skills/sup_dex.txt | 2 +- src/Modules/CalcDefence.lua | 130 +++++----- src/Modules/CalcOffence.lua | 10 +- src/Modules/CalcPerform.lua | 394 ++++++++++++++++++++++--------- src/Modules/CalcSections.lua | 6 + src/Modules/ConfigOptions.lua | 27 ++- 8 files changed, 663 insertions(+), 188 deletions(-) diff --git a/spec/System/TestDefence_spec.lua b/spec/System/TestDefence_spec.lua index 29753a271ac..e0a0060261c 100644 --- a/spec/System/TestDefence_spec.lua +++ b/spec/System/TestDefence_spec.lua @@ -618,6 +618,7 @@ describe("TestDefence", function() 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" }, @@ -637,18 +638,132 @@ describe("TestDefence", function() end) 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.TotalStoneGolemLife = 1000 + 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) - assert.are.equals(1000, output.TotalStoneGolemLife) - local pools = build.calcsTab.calcs.reducePoolsByDamage(nil, { Physical = 100 }, build.calcsTab.calcsEnv.player) - assert.are.equals(985, pools.AlliesTakenBeforeYou.stoneGolem.remaining) + 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() @@ -658,7 +773,6 @@ describe("TestDefence", function() 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.TotalStoneGolemLife = 1000 build.configTab.input.enemyDamageType = "Spell" build.configTab:BuildModList() runCallback("OnFrame") @@ -666,6 +780,158 @@ describe("TestDefence", function() 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) 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 4040362925c..72af401ae58 100644 --- a/src/Modules/CalcDefence.lua +++ b/src/Modules/CalcDefence.lua @@ -28,16 +28,17 @@ 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 = "frostShield", life = "FrostShieldLife", mitigation = "FrostShieldDamageMitigation", label = "Frost Shield Life" }, - { key = "minion", life = "TotalMinionLife", mitigation = "MinionAllyDamageMitigation", label = "Total Minion Life" }, - { key = "spectres", life = "TotalSpectreLife", mitigation = "SpectreAllyDamageMitigation", label = "Total Spectre Life" }, - { key = "totems", life = "TotalTotemLife", mitigation = "TotemAllyDamageMitigation", label = "Total Totem Life" }, - { key = "vaalRejuvenationTotems", life = "TotalVaalRejuvenationTotemLife", mitigation = "VaalRejuvenationTotemAllyDamageMitigation", label = "Total Vaal Rejuvenation Totem Life" }, - { key = "radianceSentinel", life = "TotalRadianceSentinelLife", mitigation = "RadianceSentinelAllyDamageMitigation", label = "Total Sentinel of Radiance Life" }, - { key = "voidSpawn", life = "TotalVoidSpawnLife", mitigation = "VoidSpawnAllyDamageMitigation", label = "Total Void Spawn Life" }, - { key = "stoneGolem", life = "TotalStoneGolemLife", mitigation = "StoneGolemAllyDamageMitigation", label = "Total Stone Golem Life" }, + { 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) @@ -46,7 +47,7 @@ local function buildAllyLifePools(output) 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 = mitigation / 100 } + pools[ally.key] = { remaining = life, percent = m_min(mitigation, 100) / 100 } end end return pools @@ -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 @@ -2449,47 +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") - 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") + -- 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 Sentinel of Radiance - output["RadianceSentinelAllyDamageMitigation"] = modDB:Sum("BASE", nil, "takenFromRadianceSentinelBeforeYou") - if output["RadianceSentinelAllyDamageMitigation"] ~= 0 then - output["TotalRadianceSentinelLife"] = modDB:Sum("BASE", nil, "TotalRadianceSentinelLife") + + -- 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 Void Spawn - output["VoidSpawnAllyDamageMitigation"] = modDB:Sum("BASE", nil, "takenFromVoidSpawnBeforeYou") - if output["VoidSpawnAllyDamageMitigation"] ~= 0 then - output["TotalVoidSpawnLife"] = modDB:Sum("BASE", nil, "TotalVoidSpawnLife") + + -- 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 Stone Golem of Safeguarding - local stoneGolemMitigation = modDB:Sum("BASE", nil, "takenFromStoneGolemBeforeYou") - output["StoneGolemAllyDamageMitigation"] = damageCategoryConfig == "Melee" and stoneGolemMitigation or damageCategoryConfig == "Average" and stoneGolemMitigation / 4 or 0 - if output["StoneGolemAllyDamageMitigation"] ~= 0 then - output["TotalStoneGolemLife"] = modDB:Sum("BASE", nil, "TotalStoneGolemLife") + 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 @@ -3122,13 +3140,19 @@ 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 - for _, ally in ipairs(allyLifePoolList) do + -- 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 - 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) + 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 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 067556fd467..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" }, diff --git a/src/Modules/ConfigOptions.lua b/src/Modules/ConfigOptions.lua index 89b35eb3a54..afbdd1fe8c2 100644 --- a/src/Modules/ConfigOptions.lua +++ b/src/Modules/ConfigOptions.lua @@ -786,23 +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 = "TotalStoneGolemLife", type = "integer", label = "Total Stone Golem Life:", ifSkill = "Summon Stone Golem of Safeguarding", ifMod = "takenFromStoneGolemBeforeYou", tooltip = "The total life of your Stone Golems of Safeguarding that can be taken before yours", apply = function(val, modList, enemyModList) - modList:NewMod("TotalStoneGolemLife", "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 = "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 = "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." },