From 2257923337c7649612006624706cf239dd58f24b Mon Sep 17 00:00:00 2001
From: Regisle <49933620+Regisle@users.noreply.github.com>
Date: Wed, 14 Aug 2024 20:04:45 +0930
Subject: [PATCH] Add support for pseudo recoup like divine shield and
 juggernaut (#6833)

* Add support for pseudo recoup like divine shield and juggernaut

* fix spelling
---
 src/Data/ModCache.lua        |  6 ++--
 src/Modules/CalcDefence.lua  | 65 ++++++++++++++++++++++++++++++++++--
 src/Modules/CalcSections.lua | 20 ++++++++---
 src/Modules/ModParser.lua    |  5 +++
 4 files changed, 85 insertions(+), 11 deletions(-)

diff --git a/src/Data/ModCache.lua b/src/Data/ModCache.lua
index de35be0099..5efba61e67 100755
--- a/src/Data/ModCache.lua
+++ b/src/Data/ModCache.lua
@@ -1981,9 +1981,8 @@ c["1.5% of Lightning Damage is Leeched as Mana while affected by Wrath"]={{[1]={
 c["1.5% of Physical Attack Damage Leeched as Life"]={{[1]={flags=1,keywordFlags=0,name="PhysicalDamageLifeLeech",type="BASE",value=1.5}},nil}
 c["1.5% of Physical Attack Damage Leeched as Mana"]={{[1]={flags=1,keywordFlags=0,name="PhysicalDamageManaLeech",type="BASE",value=1.5}},nil}
 c["1.5% of Physical Damage prevented from Hits in the past"]={{[1]={flags=0,keywordFlags=0,name="PhysicalDamage",type="BASE",value=1.5}},"  prevented from Hits in the past "}
-c["1.5% of Physical Damage prevented from Hits in the past 10 seconds is Regenerated as Life per second"]={{[1]={flags=0,keywordFlags=0,name="PhysicalDamage",type="BASE",value=1.5}},"  prevented from Hits in the past 10 seconds is Regenerated as Life per second "}
+c["1.5% of Physical Damage prevented from Hits in the past 10 seconds is Regenerated as Life per second"]={{[1]={flags=0,keywordFlags=0,name="PhysicalDamageMitigatedLifePseudoRecoup",type="BASE",value=15},[2]={flags=0,keywordFlags=0,name="PhysicalDamageMitigatedLifePseudoRecoupDuration",type="BASE",value="10"}},nil}
 c["1.6% of Physical Attack Damage Leeched as Life"]={{[1]={flags=1,keywordFlags=0,name="PhysicalDamageLifeLeech",type="BASE",value=1.6}},nil}
-c["10 seconds is Regenerated as Life per second"]={{[1]={flags=0,keywordFlags=0,name="Life",type="BASE",value=10}}," seconds is Regenerated as  per second "}
 c["10% Chance for Traps to Trigger an additional time"]={{},"  to Trigger an additional time "}
 c["10% Chance to Block Attack Damage"]={{[1]={flags=0,keywordFlags=0,name="BlockChance",type="BASE",value=10}},nil}
 c["10% Chance to Block Attack Damage during Effect"]={{[1]={[1]={type="Condition",var="UsingFlask"},flags=0,keywordFlags=0,name="BlockChance",type="BASE",value=10}},nil}
@@ -4291,8 +4290,7 @@ c["3% of Damage from Hits is taken from your nearest Totem's Life before you"]={
 c["3% of Damage is taken from Mana before Life"]={{[1]={flags=0,keywordFlags=0,name="DamageTakenFromManaBeforeLife",type="BASE",value=3}},nil}
 c["3% of Life Regenerated per Second if you've dealt a Critical Strike in the past 8 seconds"]={{[1]={[1]={type="Condition",var="CritInPast8Sec"},flags=0,keywordFlags=0,name="LifeRegenPercent",type="BASE",value=3}},nil}
 c["3% of Physical Attack Damage Leeched as Life"]={{[1]={flags=1,keywordFlags=0,name="PhysicalDamageLifeLeech",type="BASE",value=3}},nil}
-c["3% of Physical Damage prevented from Hits Recently is Regenerated as Energy Shield per second"]={{[1]={flags=0,keywordFlags=0,name="PhysicalDamage",type="BASE",value=3}},"  prevented from Hits Recently is Regenerated as Energy Shield per second "}
-c["3% of Physical Damage prevented from Hits Recently is Regenerated as Energy Shield per second Limited to 1 Keystone Tattoo"]={{[1]={flags=0,keywordFlags=0,name="PhysicalDamage",type="BASE",value=3}},"  prevented from Hits Recently is Regenerated as Energy Shield per second Limited to 1 Keystone Tattoo "}
+c["3% of Physical Damage prevented from Hits Recently is Regenerated as Energy Shield per second"]={{[1]={flags=0,keywordFlags=0,name="PhysicalDamageMitigatedEnergyShieldPseudoRecoup",type="BASE",value=12}},nil}
 c["3% reduced Attack and Cast Speed per Frenzy Charge"]={{[1]={[1]={type="Multiplier",var="FrenzyCharge"},flags=0,keywordFlags=0,name="Speed",type="INC",value=-3}},nil}
 c["3% reduced Cost of Skills"]={{[1]={flags=0,keywordFlags=0,name="Cost",type="INC",value=-3}},nil}
 c["3% reduced Mana Cost of Skills"]={{[1]={flags=0,keywordFlags=0,name="ManaCost",type="INC",value=-3}},nil}
diff --git a/src/Modules/CalcDefence.lua b/src/Modules/CalcDefence.lua
index dd4f3da15b..ff7c8f30e7 100644
--- a/src/Modules/CalcDefence.lua
+++ b/src/Modules/CalcDefence.lua
@@ -1246,6 +1246,36 @@ function calcs.defence(env, actor)
 				end
 			end
 		end
+		
+		-- pseudo recoup (eg %physical damage prevented from hits regenerated)
+		for _, resource in ipairs(recoupTypeList) do
+			if not modDB:Flag(nil, "No"..resource.."Regen") and not modDB:Flag(nil, "CannotGain"..resource) then
+				local PhysicalDamageMitigatedPseudoRecoup = modDB:Sum("BASE", nil, "PhysicalDamageMitigated"..resource.."PseudoRecoup")
+				if PhysicalDamageMitigatedPseudoRecoup > 0 then
+					output["PhysicalDamageMitigated"..resource.."PseudoRecoupDuration"] = modDB:Sum("BASE", nil, "PhysicalDamageMitigated"..resource.."PseudoRecoupDuration")
+					if output["PhysicalDamageMitigated"..resource.."PseudoRecoupDuration"] == 0 then
+						output["PhysicalDamageMitigated"..resource.."PseudoRecoupDuration"] = 4
+					end
+					local inc = modDB:Sum("INC", nil, resource.."Regen")
+					local more = modDB:More(nil, resource.."Regen")
+					output["PhysicalDamageMitigated"..resource.."PseudoRecoup"] = PhysicalDamageMitigatedPseudoRecoup * (1 + inc/100) * more * output[resource.."RecoveryRateMod"]
+					output["anyRecoup"] = output["anyRecoup"] + output["PhysicalDamageMitigated"..resource.."PseudoRecoup"]
+					if breakdown then
+						breakdown["PhysicalDamageMitigated"..resource.."PseudoRecoup"] = { }
+						if output[resource.."RecoveryRateMod"] ~= 1 or inc ~= 0 or more ~= 1 then
+							t_insert(breakdown["PhysicalDamageMitigated"..resource.."PseudoRecoup"], s_format("%d%% ^8(base)", PhysicalDamageMitigatedPseudoRecoup))
+							if inc ~= 0 or more ~= 1 then
+								t_insert(breakdown["PhysicalDamageMitigated"..resource.."PseudoRecoup"], s_format("* %.2f ^8(regeneration rate modifier)", (1 + inc/100) * more))
+							end
+							if output[resource.."RecoveryRateMod"] ~= 1 then
+								t_insert(breakdown["PhysicalDamageMitigated"..resource.."PseudoRecoup"], s_format("* %.2f ^8(recovery rate modifier)", output[resource.."RecoveryRateMod"]))
+							end
+						end
+						t_insert(breakdown["PhysicalDamageMitigated"..resource.."PseudoRecoup"], s_format("= %.1f%% over %d seconds", output["PhysicalDamageMitigated"..resource.."PseudoRecoup"], output["PhysicalDamageMitigated"..resource.."PseudoRecoupDuration"]))
+					end
+				end
+			end
+		end
 	end
 
 	-- Ward recharge
@@ -2666,6 +2696,8 @@ function calcs.buildDefenceEstimations(env, actor)
 	if output["anyRecoup"] > 0 and damageCategoryConfig ~= "DamageOverTime" then
 		local totalDamage = 0
 		local totalElementalDamage = 0
+		local totalPhysicalDamageMitigated = output["NumberOfMitigatedDamagingHits"] * (output.PhysicalTakenDamage - output.PhysicalTakenHit)
+		local extraPseudoRecoup = {}
 		for _, damageType in ipairs(dmgTypeList) do
 			totalDamage = totalDamage + output[damageType.."PoolLost"]
 			if isElemental[damageType] then
@@ -2673,7 +2705,7 @@ function calcs.buildDefenceEstimations(env, actor)
 			end
 		end
 		local recoupTypeList = {"Life", "Mana", "EnergyShield"}
-		for _, recoupType in ipairs(recoupTypeList) do
+		for i, recoupType in ipairs(recoupTypeList) do
 			local recoupTime = (modDB:Flag(nil, "3Second"..recoupType.."Recoup") or modDB:Flag(nil, "3SecondRecoup")) and 3 or 4
 			output["Total"..recoupType.."RecoupRecovery"] = (output[recoupType.."Recoup"] or 0) / 100 * totalDamage
 			if (output["Elemental"..recoupType.."Recoup"] or 0) > 0 and totalElementalDamage > 0 then
@@ -2684,8 +2716,19 @@ function calcs.buildDefenceEstimations(env, actor)
 					output["Total"..recoupType.."RecoupRecovery"] = output["Total"..recoupType.."RecoupRecovery"] + output[damageType..recoupType.."Recoup"] / 100 * output[damageType.."PoolLost"]
 				end
 			end
-			output[recoupType.."RecoupRecoveryMax"] = output["Total"..recoupType.."RecoupRecovery"] / recoupTime
-			output[recoupType.."RecoupRecoveryAvg"] = output["Total"..recoupType.."RecoupRecovery"] / (output["EHPsurvivalTime"] + recoupTime)
+			output["Total"..recoupType.."PseudoRecoup"] = (output["PhysicalDamageMitigated"..recoupType.."PseudoRecoup"] or 0) / 100 * totalPhysicalDamageMitigated
+			local PseudoRecoupDuration = (output["PhysicalDamageMitigated"..recoupType.."PseudoRecoupDuration"] or 4)
+			 -- Pious Path
+			if output["Total"..recoupType.."PseudoRecoup"] ~= 0 then
+				for j=i+1,#recoupTypeList do
+					if modDB:Flag(nil, recoupType.."RegenerationRecovers"..recoupTypeList[j]) and not modDB:Flag(nil, "UnaffectedBy"..recoupTypeList[j].."Regen") and not modDB:Flag(nil, "No"..recoupTypeList[j].."Regen") and not modDB:Flag(nil, "CannotGain"..recoupTypeList[j]) then
+						extraPseudoRecoup[recoupTypeList[j]] = { output["Total"..recoupType.."PseudoRecoup"] * output[recoupTypeList[j].."RecoveryRateMod"] / output[recoupType.."RecoveryRateMod"], PseudoRecoupDuration }
+					end
+				end
+			end
+			output["Total"..recoupType.."PseudoRecoup"] = ((not modDB:Flag(nil, "UnaffectedBy"..recoupType.."Regen")) and output["Total"..recoupType.."PseudoRecoup"] or 0)
+			output[recoupType.."RecoupRecoveryMax"] = output["Total"..recoupType.."RecoupRecovery"] / recoupTime + output["Total"..recoupType.."PseudoRecoup"] / PseudoRecoupDuration + (extraPseudoRecoup[recoupType] and (extraPseudoRecoup[recoupType][1] / extraPseudoRecoup[recoupType][2]) or 0)
+			output[recoupType.."RecoupRecoveryAvg"] = output["Total"..recoupType.."RecoupRecovery"] / (output["EHPsurvivalTime"] + recoupTime) + output["Total"..recoupType.."PseudoRecoup"] / (output["EHPsurvivalTime"] + PseudoRecoupDuration) + (extraPseudoRecoup[recoupType] and (extraPseudoRecoup[recoupType][1] / (output["EHPsurvivalTime"] + extraPseudoRecoup[recoupType][2])) or 0)
 			if breakdown then
 				local multipleTypes = 0
 				breakdown[recoupType.."RecoupRecoveryMax"] = { }
@@ -2715,8 +2758,24 @@ function calcs.buildDefenceEstimations(env, actor)
 				t_insert(breakdown[recoupType.."RecoupRecoveryMax"], s_format("= %d ^8(total damage recoup amount)", output["Total"..recoupType.."RecoupRecovery"]))
 				breakdown[recoupType.."RecoupRecoveryAvg"] = copyTable(breakdown[recoupType.."RecoupRecoveryMax"])
 				t_insert(breakdown[recoupType.."RecoupRecoveryMax"], s_format("/ %.2f ^8(over %d seconds)", recoupTime, recoupTime))
+				if output["Total"..recoupType.."PseudoRecoup"] > 0 then
+					t_insert(breakdown[recoupType.."RecoupRecoveryMax"], s_format("+ %.2f ^8(total damage mitigated pseudo recoup amount)", output["Total"..recoupType.."PseudoRecoup"]))
+					t_insert(breakdown[recoupType.."RecoupRecoveryMax"], s_format("/ %.2f ^8(over %d seconds)", PseudoRecoupDuration, PseudoRecoupDuration))
+				end
+				if extraPseudoRecoup[recoupType] then
+					t_insert(breakdown[recoupType.."RecoupRecoveryMax"], s_format("+ %.2f ^8(total damage mitigated pseudo recoup amount)", extraPseudoRecoup[recoupType][1]))
+					t_insert(breakdown[recoupType.."RecoupRecoveryMax"], s_format("/ %.2f ^8(over %d seconds)", extraPseudoRecoup[recoupType][2], extraPseudoRecoup[recoupType][2]))
+				end
 				t_insert(breakdown[recoupType.."RecoupRecoveryMax"], s_format("= %.2f per second ^8", output[recoupType.."RecoupRecoveryMax"]))
 				t_insert(breakdown[recoupType.."RecoupRecoveryAvg"], s_format("/ %.2f ^8(total time of the recoup (survival time + %d seconds))", (output["EHPsurvivalTime"] + recoupTime), recoupTime))
+				if output["Total"..recoupType.."PseudoRecoup"] > 0 then
+					t_insert(breakdown[recoupType.."RecoupRecoveryAvg"], s_format("+ %.2f ^8(total damage mitigated pseudo recoup amount)", output["Total"..recoupType.."PseudoRecoup"]))
+					t_insert(breakdown[recoupType.."RecoupRecoveryAvg"], s_format("/ %.2f ^8(total time of the recoup (survival time + %d seconds)", (output["EHPsurvivalTime"] + PseudoRecoupDuration), PseudoRecoupDuration))
+				end
+				if extraPseudoRecoup[recoupType] then
+					t_insert(breakdown[recoupType.."RecoupRecoveryAvg"], s_format("+ %.2f ^8(total damage mitigated pseudo recoup amount)", extraPseudoRecoup[recoupType][1]))
+					t_insert(breakdown[recoupType.."RecoupRecoveryAvg"], s_format("/ %.2f ^8(total time of the recoup (survival time + %d seconds)", (output["EHPsurvivalTime"] + extraPseudoRecoup[recoupType][2]), extraPseudoRecoup[recoupType][2]))
+				end
 				t_insert(breakdown[recoupType.."RecoupRecoveryAvg"], s_format("= %.2f per second ^8", output[recoupType.."RecoupRecoveryAvg"]))
 			end
 		end
diff --git a/src/Modules/CalcSections.lua b/src/Modules/CalcSections.lua
index e0d52333ec..696b4c00dd 100644
--- a/src/Modules/CalcSections.lua
+++ b/src/Modules/CalcSections.lua
@@ -1401,6 +1401,12 @@ return {
 		{ label = "Recovery modifiers", modName = "LifeRecoveryRate" },
 		{ label = "FasterRecoup", modName = { "3SecondRecoup", "3SecondLifeRecoup" } },
 	}, },
+	{ label = "Dmg. Mit. Regen", haveOutput = "PhysicalDamageMitigatedLifePseudoRecoup", { format = "{1:output:PhysicalDamageMitigatedLifePseudoRecoup}%", { breakdown = "PhysicalDamageMitigatedLifePseudoRecoup" }, 
+		{ label = "Sources", modName = "PhysicalDamageMitigatedLifePseudoRecoup" },
+		{ label = "Increased Life Regeneration Rate", modName = { "LifeRegen" }, modType = "INC" },
+		{ label = "More Life Regeneration Rate", modName = { "LifeRegen" }, modType = "MORE" },
+		{ label = "Recovery modifiers", modName = "LifeRecoveryRate" },
+	}, },
 } }
 } },
 { 1, "Mana", 2, colorCodes.MANA, {{ defaultCollapsed = false, label = "Mana", data = {
@@ -1461,6 +1467,12 @@ return {
 		{ label = "Recovery modifiers", modName = "EnergyShieldRecoveryRate" },
 		{ label = "FasterRecoup", modName = "3SecondRecoup" },
 	}, },
+	{ label = "Dmg. Mit. Regen", haveOutput = "PhysicalDamageMitigatedEnergyShieldPseudoRecoup", { format = "{1:output:PhysicalDamageMitigatedEnergyShieldPseudoRecoup}%", { breakdown = "PhysicalDamageMitigatedEnergyShieldPseudoRecoup" }, 
+		{ label = "Sources", modName = "PhysicalDamageMitigatedEnergyShieldPseudoRecoup" },
+		{ label = "Increased Energy Shield Regeneration Rate", modName = { "EnergyShieldRegen" }, modType = "INC" },
+		{ label = "More Energy Shield Regeneration Rate", modName = { "EnergyShieldRegen" }, modType = "MORE" },
+		{ label = "Recovery modifiers", modName = "EnergyShieldRecoveryRate" },
+	}, },
 } }
 } },
 { 1, "Ward", 2, colorCodes.WARD, {{ defaultCollapsed = false, label = "Ward", data = {
@@ -2175,7 +2187,7 @@ return {
 	{ label = "Recoup Max.", haveOutput = "anyRecoup", 
 		{ format = "{0:output:LifeRecoupRecoveryMax}",
 			{ breakdown = "LifeRecoupRecoveryMax" },
-			{ label = "Sources", modName = { "LifeRecoup", "PhysicalLifeRecoup", "LightningLifeRecoup", "ColdLifeRecoup", "FireLifeRecoup", "ChaosLifeRecoup" } },
+			{ label = "Sources", modName = { "LifeRecoup", "PhysicalLifeRecoup", "LightningLifeRecoup", "ColdLifeRecoup", "FireLifeRecoup", "ChaosLifeRecoup", "PhysicalDamageMitigatedLifePseudoRecoup" } },
 			{ label = "Recovery modifiers", modName = "LifeRecoveryRate" },
 			{ label = "Faster Recoup", modName = "3SecondRecoup" },
 		},
@@ -2187,7 +2199,7 @@ return {
 		},
 		{ format = "{0:output:EnergyShieldRecoupRecoveryMax}",
 			{ breakdown = "EnergyShieldRecoupRecoveryMax" },
-			{ label = "Sources", modName = { "EnergyShieldRecoup", "ElementalEnergyShieldRecoup" } },
+			{ label = "Sources", modName = { "EnergyShieldRecoup", "ElementalEnergyShieldRecoup", "PhysicalDamageMitigatedEnergyShieldPseudoRecoup" } },
 			{ label = "Recovery modifiers", modName = "EnergyShieldRecoveryRate" },
 			{ label = "Faster Recoup", modName = "3SecondRecoup" },
 		},
@@ -2195,7 +2207,7 @@ return {
 	{ label = "Recoup Avg.", haveOutput = "anyRecoup", 
 		{ format = "{0:output:LifeRecoupRecoveryAvg}",
 			{ breakdown = "LifeRecoupRecoveryAvg" },
-			{ label = "Sources", modName = { "LifeRecoup", "PhysicalLifeRecoup", "LightningLifeRecoup", "ColdLifeRecoup", "FireLifeRecoup", "ChaosLifeRecoup" } },
+			{ label = "Sources", modName = { "LifeRecoup", "PhysicalLifeRecoup", "LightningLifeRecoup", "ColdLifeRecoup", "FireLifeRecoup", "ChaosLifeRecoup", "PhysicalDamageMitigatedLifePseudoRecoup" } },
 			{ label = "Recovery modifiers", modName = "LifeRecoveryRate" },
 			{ label = "Faster Recoup", modName = "3SecondRecoup" },
 		},
@@ -2207,7 +2219,7 @@ return {
 		},
 		{ format = "{0:output:EnergyShieldRecoupRecoveryAvg}",
 			{ breakdown = "EnergyShieldRecoupRecoveryAvg" },
-			{ label = "Sources", modName = { "EnergyShieldRecoup", "ElementalEnergyShieldRecoup" } },
+			{ label = "Sources", modName = { "EnergyShieldRecoup", "ElementalEnergyShieldRecoup", "PhysicalDamageMitigatedEnergyShieldPseudoRecoup" } },
 			{ label = "Recovery modifiers", modName = "EnergyShieldRecoveryRate" },
 			{ label = "Faster Recoup", modName = "3SecondRecoup" },
 		},
diff --git a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua
index 8973b79445..d6b4563871 100644
--- a/src/Modules/ModParser.lua
+++ b/src/Modules/ModParser.lua
@@ -4141,6 +4141,11 @@ local specialModList = {
 	["(%d+)%% of damage taken while affected by clarity recouped as mana"] = function(num) return { mod("ManaRecoup", "BASE", num, { type = "Condition", var = "AffectedByClarity" }) } end,
 	["recoup effects instead occur over 3 seconds"] = { flag("3SecondRecoup") },
 	["life recoup effects instead occur over 3 seconds"] = { flag("3SecondLifeRecoup") },
+	["([%d%.]+)%% of physical damage prevented from hits in the past (%d+) seconds is regenerated as life per second"] = function(num, _, duration) return { 
+		mod("PhysicalDamageMitigatedLifePseudoRecoup", "BASE", num * duration), 
+		mod("PhysicalDamageMitigatedLifePseudoRecoupDuration", "BASE", duration),
+	} end,
+	["([%d%.]+)%% of physical damage prevented from hits recently is regenerated as energy shield per second"] = function(num) return { mod("PhysicalDamageMitigatedEnergyShieldPseudoRecoup", "BASE", num * 4) } end,
 	["cannot leech or regenerate mana"] = { flag("NoManaRegen"), flag("CannotLeechMana") },
 	["right ring slot: you cannot regenerate mana" ] = { flag("NoManaRegen", { type = "SlotNumber", num = 2 }) },
 	["y?o?u? ?cannot recharge energy shield"] = { flag("NoEnergyShieldRecharge") },