Skip to content

Commit

Permalink
Improve EHP accuracy (#8430)
Browse files Browse the repository at this point in the history
* Improve EHP accuracy

* Hunting more corner cases

MoM with ES when there is ES bypass was not capping effective ES.
MoM with life loss prevention was not accounting for the multiplied life effectiveness when reducing pools.
Max hit smoothing (for conversion + armour) now uses pool reduction to effectively home in on a precise max hit.

* Hunting more corner cases

MoM with ES when there is ES bypass was not capping effective ES.
MoM with life loss prevention was not accounting for the multiplied life effectiveness when reducing pools.
Max hit smoothing (for conversion + armour) now uses pool reduction to effectively home in on a precise max hit.
  • Loading branch information
Edvinas-Smita authored Feb 12, 2025
1 parent b6b326f commit 195006f
Show file tree
Hide file tree
Showing 3 changed files with 256 additions and 225 deletions.
315 changes: 160 additions & 155 deletions spec/System/TestDefence_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe("TestDefence", function()
build.configTab.input.enemyIsBoss = "None"
build.configTab:BuildModList()
runCallback("OnFrame")

assert.are.equals(60, build.calcsTab.calcsOutput.PhysicalMaximumHitTaken)
assert.are.equals(38, build.calcsTab.calcsOutput.FireMaximumHitTaken)
assert.are.equals(38, build.calcsTab.calcsOutput.ColdMaximumHitTaken)
Expand Down Expand Up @@ -95,73 +95,76 @@ describe("TestDefence", function()
end

it("progenesis and petrified blood", function()
build.configTab.input.enemyIsBoss = "None"
-- Petrified blood
--build.skillsTab:PasteSocketGroup("\z
--Label: 50% petrified\n\z
--Petrified Blood 20/40 Alternate1 1\n\z
--Arrogance 21/200 Alternate1 1\n\z
--") -- 50% petrified effect, when exactly half of the life is reserved, should make the life pool be equivalent to no petrified effect and full life.
--build.skillsTab:ProcessSocketGroup(build.skillsTab.socketGroupList[1])
--build.configTab.input.customMods = "\z
--+200 to all resistances\n\z
--+200 to all maximum resistances\n\z
--50% reduced damage taken\n\z
--50% less damage taken\n\z
--Nearby enemies deal 20% less damage\n\z
--"
--build.configTab:BuildModList()
--runCallback("OnFrame")
--assert.are.equals(300, build.calcsTab.calcsOutput.PhysicalMaximumHitTaken)
--assert.are.equals(3000, build.calcsTab.calcsOutput.FireMaximumHitTaken)
--assert.are.equals(3000, build.calcsTab.calcsOutput.ColdMaximumHitTaken)
--assert.are.equals(3000, build.calcsTab.calcsOutput.LightningMaximumHitTaken)
--assert.are.equals(3000, build.calcsTab.calcsOutput.ChaosMaximumHitTaken)
--build.skillsTab.socketGroupList = {}
--
--build.skillsTab:PasteSocketGroup("\z
--Label: 50% petrified\n\z
--Petrified Blood 20/40 Alternate1 1\n\z
--Arrogance 21/200 Alternate1 1\n\z
--")
--build.skillsTab:ProcessSocketGroup(build.skillsTab.socketGroupList[1])
--build.configTab.input.customMods = "\z
--+200 to all resistances\n\z
--+200 to all maximum resistances\n\z
--50% reduced damage taken\n\z
--50% less damage taken\n\z
--Nearby enemies deal 20% less damage\n\z
--100% less intelligence\n\z
--+60 to maximum energy shield\n\z
--" -- petrified blood should not interact with pools other than life.
--build.configTab:BuildModList()
--runCallback("OnFrame")
--assert.are.equals(600, build.calcsTab.calcsOutput.PhysicalMaximumHitTaken)
--assert.are.equals(6000, build.calcsTab.calcsOutput.FireMaximumHitTaken)
--assert.are.equals(6000, build.calcsTab.calcsOutput.ColdMaximumHitTaken)
--assert.are.equals(6000, build.calcsTab.calcsOutput.LightningMaximumHitTaken)
--assert.are.equals(3000, build.calcsTab.calcsOutput.ChaosMaximumHitTaken)
--build.skillsTab.socketGroupList = {}
--
--build.skillsTab:PasteSocketGroup("\z
--Label: 75% petrified\n\z
--Petrified Blood 20/140 Alternate1 1\n\z
--") -- 75% petrified effect, starting from full life, should make the life pool be equivalent to 0.5 * life (unprotected upper half) and then 4 * 0.5 * life (protected lower half), making it 2.5* bigger in total
--build.skillsTab:ProcessSocketGroup(build.skillsTab.socketGroupList[1])
--build.configTab.input.customMods = "\z
--+200 to all resistances\n\z
--+200 to all maximum resistances\n\z
--50% reduced damage taken\n\z
--50% less damage taken\n\z
--Nearby enemies deal 20% less damage\n\z
--"
--build.configTab:BuildModList()
--runCallback("OnFrame")
--assert.are.equals(750, build.calcsTab.calcsOutput.PhysicalMaximumHitTaken)
--assert.are.equals(7500, build.calcsTab.calcsOutput.FireMaximumHitTaken)
--assert.are.equals(7500, build.calcsTab.calcsOutput.ColdMaximumHitTaken)
--assert.are.equals(7500, build.calcsTab.calcsOutput.LightningMaximumHitTaken)
--assert.are.equals(7500, build.calcsTab.calcsOutput.ChaosMaximumHitTaken)
--build.skillsTab.socketGroupList = {}
build.skillsTab:PasteSocketGroup("\z
Petrified Blood 20/0 Default 1\n\z
Arrogance 21/0 Default 1\n\z
") -- 50% petrified effect, when exactly half of the life is reserved, should make the life pool be equivalent to no petrified effect and full life.
build.skillsTab:ProcessSocketGroup(build.skillsTab.socketGroupList[1])
build.configTab.input.customMods = "\z
+200 to all resistances\n\z
+200 to all maximum resistances\n\z
50% reduced damage taken\n\z
50% less damage taken\n\z
Nearby enemies deal 20% less damage\n\z
40% increased reservation efficiency\n\z
25% increased effect of buffs on you\n\z
"
build.configTab:BuildModList()
runCallback("OnFrame")
assert.are.equals(300, build.calcsTab.calcsOutput.PhysicalMaximumHitTaken)
assert.are.equals(3000, build.calcsTab.calcsOutput.FireMaximumHitTaken)
assert.are.equals(3000, build.calcsTab.calcsOutput.ColdMaximumHitTaken)
assert.are.equals(3000, build.calcsTab.calcsOutput.LightningMaximumHitTaken)
assert.are.equals(3000, build.calcsTab.calcsOutput.ChaosMaximumHitTaken)
build.skillsTab.socketGroupList = {}

build.skillsTab:PasteSocketGroup("\z
Petrified Blood 20/0 Default 1\n\z
Arrogance 21/0 Default 1\n\z
")
build.skillsTab:ProcessSocketGroup(build.skillsTab.socketGroupList[1])
build.configTab.input.customMods = "\z
+200 to all resistances\n\z
+200 to all maximum resistances\n\z
50% reduced damage taken\n\z
50% less damage taken\n\z
Nearby enemies deal 20% less damage\n\z
100% less intelligence\n\z
+60 to maximum energy shield\n\z
40% increased reservation efficiency\n\z
25% increased effect of buffs on you\n\z
" -- petrified blood should not interact with pools other than life.
build.configTab:BuildModList()
runCallback("OnFrame")
assert.are.equals(600, build.calcsTab.calcsOutput.PhysicalMaximumHitTaken)
assert.are.equals(6000, build.calcsTab.calcsOutput.FireMaximumHitTaken)
assert.are.equals(6000, build.calcsTab.calcsOutput.ColdMaximumHitTaken)
assert.are.equals(6000, build.calcsTab.calcsOutput.LightningMaximumHitTaken)
assert.are.equals(3000, build.calcsTab.calcsOutput.ChaosMaximumHitTaken)
build.skillsTab.socketGroupList = {}

build.skillsTab:PasteSocketGroup("\z
Petrified Blood 20/0 Default 1\n\z
") -- 80% petrified effect, starting from full life, should make the life pool be equivalent to 0.5 * life (unprotected upper half) and then 5 * 0.5 * life (protected lower half), making it 3* bigger in total
build.skillsTab:ProcessSocketGroup(build.skillsTab.socketGroupList[1])
build.configTab.input.customMods = "\z
+200 to all resistances\n\z
+200 to all maximum resistances\n\z
50% reduced damage taken\n\z
50% less damage taken\n\z
Nearby enemies deal 20% less damage\n\z
100% increased effect of buffs on you\n\z
"
build.configTab:BuildModList()
runCallback("OnFrame")
assert.are.equals(900, build.calcsTab.calcsOutput.PhysicalMaximumHitTaken)
assert.are.equals(9000, build.calcsTab.calcsOutput.FireMaximumHitTaken)
assert.are.equals(9000, build.calcsTab.calcsOutput.ColdMaximumHitTaken)
assert.are.equals(9000, build.calcsTab.calcsOutput.LightningMaximumHitTaken)
assert.are.equals(9000, build.calcsTab.calcsOutput.ChaosMaximumHitTaken)
build.skillsTab.socketGroupList = {}

-- Progenesis
build.configTab.input.customMods = "\z
Expand All @@ -173,7 +176,6 @@ describe("TestDefence", function()
When Hit during effect, 50% of Life loss from Damage taken occurs over 4 seconds instead\n\z
" -- 50% progenesis should just simply double the life pool
build.configTab.input.conditionUsingFlask = true
build.configTab.input.enemyIsBoss = "None"
build.configTab:BuildModList()
runCallback("OnFrame")
assert.are.equals(600, build.calcsTab.calcsOutput.PhysicalMaximumHitTaken)
Expand Down Expand Up @@ -201,52 +203,53 @@ describe("TestDefence", function()
assert.are.equals(6000, build.calcsTab.calcsOutput.ChaosMaximumHitTaken)

-- Progenesis + petrified blood
--build.skillsTab:PasteSocketGroup("\z
--Label: 50% petrified\n\z
--Petrified Blood 20/40 Alternate1 1\n\z
--Arrogance 21/200 Alternate1 1\n\z
--")
--build.skillsTab:ProcessSocketGroup(build.skillsTab.socketGroupList[1])
--build.configTab.input.customMods = "\z
--+200 to all resistances\n\z
--+200 to all maximum resistances\n\z
--50% reduced damage taken\n\z
--50% less damage taken\n\z
--Nearby enemies deal 20% less damage\n\z
--When Hit during effect, 50% of Life loss from Damage taken occurs over 4 seconds instead\n\z
--" -- With half of life reserved, both effects are active and multiplicative with each other, making the effective life pool 4 * half life = 2 * life (or same as no petrified, no reserve and 50% progenesis)
--build.configTab.input.conditionUsingFlask = true
--build.configTab:BuildModList()
--runCallback("OnFrame")
--assert.are.equals(600, build.calcsTab.calcsOutput.PhysicalMaximumHitTaken)
--assert.are.equals(6000, build.calcsTab.calcsOutput.FireMaximumHitTaken)
--assert.are.equals(6000, build.calcsTab.calcsOutput.ColdMaximumHitTaken)
--assert.are.equals(6000, build.calcsTab.calcsOutput.LightningMaximumHitTaken)
--assert.are.equals(6000, build.calcsTab.calcsOutput.ChaosMaximumHitTaken)
--build.skillsTab.socketGroupList = {}
--
--build.skillsTab:PasteSocketGroup("\z
--Label: 50% petrified\n\z
--Petrified Blood 20/40 Alternate1 1\n\z
--")
--build.skillsTab:ProcessSocketGroup(build.skillsTab.socketGroupList[1])
--build.configTab.input.customMods = "\z
--+200 to all resistances\n\z
--+200 to all maximum resistances\n\z
--50% reduced damage taken\n\z
--50% less damage taken\n\z
--Nearby enemies deal 20% less damage\n\z
--When Hit during effect, 50% of Life loss from Damage taken occurs over 4 seconds instead\n\z
--" -- With no life reserved, progenesis first doubles the pool of life above low, then both progenesis and petrified quadruple the pool of life below low, so effective pool is 3 * life
--build.configTab.input.conditionUsingFlask = true
--build.configTab:BuildModList()
--runCallback("OnFrame")
--assert.are.equals(900, build.calcsTab.calcsOutput.PhysicalMaximumHitTaken)
--assert.are.equals(9000, build.calcsTab.calcsOutput.FireMaximumHitTaken)
--assert.are.equals(9000, build.calcsTab.calcsOutput.ColdMaximumHitTaken)
--assert.are.equals(9000, build.calcsTab.calcsOutput.LightningMaximumHitTaken)
--assert.are.equals(9000, build.calcsTab.calcsOutput.ChaosMaximumHitTaken)
--build.skillsTab.socketGroupList = {}
build.skillsTab:PasteSocketGroup("\z
Petrified Blood 20/0 Default 1\n\z
Arrogance 21/0 Default 1\n\z
")
build.skillsTab:ProcessSocketGroup(build.skillsTab.socketGroupList[1])
build.configTab.input.customMods = "\z
+200 to all resistances\n\z
+200 to all maximum resistances\n\z
50% reduced damage taken\n\z
50% less damage taken\n\z
Nearby enemies deal 20% less damage\n\z
When Hit during effect, 50% of Life loss from Damage taken occurs over 4 seconds instead\n\z
40% increased reservation efficiency\n\z
25% increased effect of buffs on you\n\z
" -- With half of life reserved, both effects are active and multiplicative with each other, making the effective life pool 4 * half life = 2 * life (or same as no petrified, no reserve and 50% progenesis)
build.configTab.input.conditionUsingFlask = true
build.configTab:BuildModList()
runCallback("OnFrame")
assert.are.equals(600, build.calcsTab.calcsOutput.PhysicalMaximumHitTaken)
assert.are.equals(6000, build.calcsTab.calcsOutput.FireMaximumHitTaken)
assert.are.equals(6000, build.calcsTab.calcsOutput.ColdMaximumHitTaken)
assert.are.equals(6000, build.calcsTab.calcsOutput.LightningMaximumHitTaken)
assert.are.equals(6000, build.calcsTab.calcsOutput.ChaosMaximumHitTaken)
build.skillsTab.socketGroupList = {}

build.skillsTab:PasteSocketGroup("\z
Petrified Blood 20/0 Default 1\n\z
")
build.skillsTab:ProcessSocketGroup(build.skillsTab.socketGroupList[1])
build.configTab.input.customMods = "\z
+200 to all resistances\n\z
+200 to all maximum resistances\n\z
50% reduced damage taken\n\z
50% less damage taken\n\z
Nearby enemies deal 20% less damage\n\z
When Hit during effect, 50% of Life loss from Damage taken occurs over 4 seconds instead\n\z
25% increased effect of buffs on you\n\z
" -- With no life reserved, progenesis first doubles the pool of life above low, then both progenesis and petrified quadruple the pool of life below low, so effective pool is 3 * life
build.configTab.input.conditionUsingFlask = true
build.configTab:BuildModList()
runCallback("OnFrame")
assert.are.equals(900, build.calcsTab.calcsOutput.PhysicalMaximumHitTaken)
assert.are.equals(9000, build.calcsTab.calcsOutput.FireMaximumHitTaken)
assert.are.equals(9000, build.calcsTab.calcsOutput.ColdMaximumHitTaken)
assert.are.equals(9000, build.calcsTab.calcsOutput.LightningMaximumHitTaken)
assert.are.equals(9000, build.calcsTab.calcsOutput.ChaosMaximumHitTaken)
build.skillsTab.socketGroupList = {}

build.skillsTab:PasteSocketGroup("\z
Petrified Blood 20/0 Default 1\n\z
Expand Down Expand Up @@ -277,57 +280,57 @@ describe("TestDefence", function()
assert.are.equals(120, poolsRemaining.LifeLossLostOverTime)
assert.are.equals(20, poolsRemaining.LifeBelowHalfLossLostOverTime)

--build.skillsTab:PasteSocketGroup("\z
--Label: 50% petrified\n\z
--Petrified Blood 20/40 Alternate1 1\n\z
--")
--build.skillsTab:ProcessSocketGroup(build.skillsTab.socketGroupList[1])
--build.configTab.input.customMods = "\z
--+1950 to life\n\z
--+2960 to mana\n\z
--+3000 to energy shield\n\z
--100% less attributes\n\z
--100% less mana reserved\n\z
--+60% to all resistances\n\z
--chaos damage does not bypass energy shield\n\z
--mind over matter\n\z
--eldritch battery\n\z
--10% of lightning damage is taken from mana before life\n\z
--chaos damage is taken from mana before life\n\z
--When Hit during effect, 50% of Life loss from Damage taken occurs over 4 seconds instead\n\z
--"
--build.configTab.input.conditionUsingFlask = true
--build.configTab:BuildModList()
--runCallback("OnFrame")
--
--_, takenDamages = takenHitFromTypeMaxHit("Fire")
--poolsRemaining = build.calcsTab.calcs.reducePoolsByDamage(nil, takenDamages, build.calcsTab.calcsEnv.player)
--assert.are.equals(0, poolsRemaining.Life)
--assert.are.equals(0, poolsRemaining.EnergyShield)
--assert.is.not_false(poolsRemaining.Mana > 0)
--
--_, takenDamages = takenHitFromTypeMaxHit("Lightning")
--poolsRemaining = build.calcsTab.calcs.reducePoolsByDamage(nil, takenDamages, build.calcsTab.calcsEnv.player)
--assert.are.equals(0, poolsRemaining.Life)
--assert.are.equals(0, poolsRemaining.EnergyShield)
--assert.are.equals(0, poolsRemaining.Mana)
--
--_, takenDamages = takenHitFromTypeMaxHit("Chaos")
--poolsRemaining = build.calcsTab.calcs.reducePoolsByDamage(nil, takenDamages, build.calcsTab.calcsEnv.player)
--assert.are.equals(0, poolsRemaining.Life)
--assert.are.equals(0, poolsRemaining.EnergyShield)
--assert.are.equals(0, poolsRemaining.Mana)
build.skillsTab:PasteSocketGroup("\z
Petrified Blood 20/0 Default 1\n\z
")
build.skillsTab:ProcessSocketGroup(build.skillsTab.socketGroupList[1])
build.configTab.input.customMods = "\z
+1950 to life\n\z
+2960 to mana\n\z
+3000 to energy shield\n\z
100% less attributes\n\z
100% less mana reserved\n\z
+60% to all resistances\n\z
chaos damage does not bypass energy shield\n\z
mind over matter\n\z
eldritch battery\n\z
10% of lightning damage is taken from mana before life\n\z
chaos damage is taken from mana before life\n\z
When Hit during effect, 50% of Life loss from Damage taken occurs over 4 seconds instead\n\z
25% increased effect of buffs on you\n\z
"
build.configTab.input.conditionUsingFlask = true
build.configTab:BuildModList()
runCallback("OnFrame")

_, takenDamages = takenHitFromTypeMaxHit("Fire")
poolsRemaining = build.calcsTab.calcs.reducePoolsByDamage(nil, takenDamages, build.calcsTab.calcsEnv.player)
assert.are.equals(0, poolsRemaining.Life)
assert.are.equals(0, poolsRemaining.EnergyShield)
assert.is.not_false(poolsRemaining.Mana > 0)

_, takenDamages = takenHitFromTypeMaxHit("Lightning")
poolsRemaining = build.calcsTab.calcs.reducePoolsByDamage(nil, takenDamages, build.calcsTab.calcsEnv.player)
assert.are.equals(0, poolsRemaining.Life)
assert.are.equals(0, poolsRemaining.EnergyShield)
assert.are.equals(0, poolsRemaining.Mana)

_, takenDamages = takenHitFromTypeMaxHit("Chaos")
poolsRemaining = build.calcsTab.calcs.reducePoolsByDamage(nil, takenDamages, build.calcsTab.calcsEnv.player)
assert.are.equals(0, poolsRemaining.Life)
assert.are.equals(0, poolsRemaining.EnergyShield)
assert.are.equals(0, poolsRemaining.Mana)

build.skillsTab.socketGroupList = {}
end)

-- fun part
it("armoured max hits", function()
build.configTab.input.enemyIsBoss = "None"
build.configTab.input.customMods = "\z
+940 to maximum life\n\z
+10000 to armour\n\z
" -- hit of 2000 on 10000 armour results in 50% DR which reduces the damage to 1000 - total HP
build.configTab.input.enemyIsBoss = "None"
build.configTab:BuildModList()
runCallback("OnFrame")
assert.are.equals(1000, takenHitFromTypeMaxHit("Physical"))
Expand Down Expand Up @@ -426,6 +429,8 @@ describe("TestDefence", function()
return 0.9 < ratio and ratio < 1.1
end
it("damage conversion max hits", function()
build.configTab.input.enemyIsBoss = "None"

build.configTab.input.customMods = "\z
+940 to maximum life\n\z
+200 to all resistances\n\z
Expand Down
Loading

0 comments on commit 195006f

Please sign in to comment.