From 1e4b3ec5241c64ebd3c883b1cb1720ddd95fd3c9 Mon Sep 17 00:00:00 2001 From: AdamZ Date: Tue, 8 Sep 2026 08:44:19 -0700 Subject: [PATCH] Respect spell cooldowns for traps/mines while calculating DPS --- spec/System/TestOffence_spec.lua | 58 ++++++++++++++++++++++++++++++++ src/Modules/CalcOffence.lua | 4 +++ 2 files changed, 62 insertions(+) diff --git a/spec/System/TestOffence_spec.lua b/spec/System/TestOffence_spec.lua index da782f9f679..0a04bc9f3d1 100644 --- a/spec/System/TestOffence_spec.lua +++ b/spec/System/TestOffence_spec.lua @@ -13,6 +13,64 @@ describe("TestOffence", function() string.format("%s: expected ~%.2f (within %.1f%%), got %.2f", msg, expected, tolerance * 100, actual)) end + for _, support in ipairs({ + { name = "Trap", cooldown = "TrapCooldown", speed = "TrapThrowingSpeed", rate = 119 / 60 }, + { name = "Blastchain Mine", cooldown = "Cooldown", speed = "MineLayingSpeed", rate = 10 / 3 }, + { name = "High-Impact Mine", cooldown = "Cooldown", speed = "MineLayingSpeed", rate = 10 / 3 }, + }) do + it("limits " .. support.name .. " hit DPS and Full DPS by cooldown without exceeding throwing speed", function() + build.skillsTab:PasteSocketGroup("Frost Bomb 20/0 1\n" .. support.name .. " 20/0 1") + build.skillsTab.socketGroupList[1].includeInFullDPS = true + for _, case in ipairs({ + { recovery = 0, cooldown = 2.508 }, + { recovery = 100, cooldown = 1.254 }, + { recovery = 1000, cooldown = 0.231 }, + }) do + build.configTab.input.customMods = case.recovery .. "% increased Cooldown Recovery Rate" + build.configTab:BuildModList() + runCallback("OnFrame") + + local output = build.calcsTab.mainOutput + local expectedRate = case.recovery == 1000 and support.rate or 1 / case.cooldown + assert.near(case.cooldown, output[support.cooldown], 0.000001) + assert.near(support.rate, output[support.speed], 0.000001) + assert.near(expectedRate, output.Speed, 0.000001) + assert.near(output.AverageDamage * expectedRate, output.TotalDPS, 0.000001) + assert.near(output.TotalDPS, output.FullDPS, 0.000001) + end + end) + + it("keeps cooldown-free " .. support.name .. " skills limited by throwing speed", function() + build.skillsTab:PasteSocketGroup("Fireball 20/0 1\n" .. support.name .. " 20/0 1") + runCallback("OnFrame") + + local output = build.calcsTab.mainOutput + assert.is_nil(output[support.cooldown]) + assert.near(support.rate, output.Speed, 0.000001) + assert.near(output.AverageDamage * support.rate, output.TotalDPS, 0.000001) + end) + end + + it("preserves Lightning Spire Trap's independent hit rate", function() + build.skillsTab:PasteSocketGroup("Lightning Spire Trap 20/0 1") + runCallback("OnFrame") + + local output = build.calcsTab.mainOutput + assert.near(8.019, output.TrapCooldown, 0.000001) + assert.near(1 / 0.36, output.HitSpeed, 0.000001) + assert.near(output.AverageDamage / 0.36, output.TotalDPS, 0.000001) + end) + + it("preserves the cooldown cap for spells used by totems", function() + build.skillsTab:PasteSocketGroup("Frost Bomb 20/0 1\nSpell Totem 20/0 1") + runCallback("OnFrame") + + local output = build.calcsTab.mainOutput + assert.near(2.508, output.Cooldown, 0.000001) + assert.near(1 / 2.508, output.Speed, 0.000001) + assert.near(output.AverageDamage / 2.508, output.TotalDPS, 0.000001) + end) + it("counts only permanent minions for Communion", function() build.skillsTab:PasteSocketGroup("Fireball 20/0 1\nCommunion 3/0 1") build.skillsTab:PasteSocketGroup("Summon Reaper 20/0 1") diff --git a/src/Modules/CalcOffence.lua b/src/Modules/CalcOffence.lua index 8458c4e468f..f71d3aeda69 100644 --- a/src/Modules/CalcOffence.lua +++ b/src/Modules/CalcOffence.lua @@ -2275,6 +2275,10 @@ function calcs.offence(env, actor, activeSkill) output.Speed = 0 elseif skillData.timeOverride then output.Time = skillData.timeOverride + local cooldown = globalOutput.TrapCooldown or globalOutput.Cooldown + if cooldown then + output.Time = m_max(output.Time, cooldown) + end output.Speed = 1 / output.Time elseif skillData.fixedCastTime then output.Time = activeSkill.activeEffect.grantedEffect.castTime