Skip to content

Commit c2ee5eb

Browse files
authored
Add Support for Nuanced Kinetic Fusillade (#9183)
* Add support for Kinetic Fusillade Introduce support for Kinetic Fusillade's sequential fire damage mod. Add support for DPS calculations considering all available projectiles while factoring in duration and attack rate for effective dps calcs. Signed-off-by: Justin Stitt <[email protected]> * show effective APS even for single proj mode Signed-off-by: Justin Stitt <[email protected]> * consider delay between projectiles for max effective APS Signed-off-by: Justin Stitt <[email protected]> --------- Signed-off-by: Justin Stitt <[email protected]>
1 parent d104804 commit c2ee5eb

File tree

2 files changed

+107
-0
lines changed

2 files changed

+107
-0
lines changed

src/Export/Skills/act_int.txt

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2265,6 +2265,111 @@ local skills, mod, flag, skill = ...
22652265

22662266
#skill KineticFusillade
22672267
#flags attack projectile area duration
2268+
parts = {
2269+
{
2270+
name = "All Projectiles",
2271+
},
2272+
{
2273+
name = "1 Projectile"
2274+
},
2275+
},
2276+
preDamageFunc = function(activeSkill, output, breakdown)
2277+
local skillData = activeSkill.skillData
2278+
local t_insert = table.insert
2279+
local s_format = string.format
2280+
2281+
if activeSkill.skillPart == 1 then
2282+
-- Set base dpsMultiplier for projectile count
2283+
activeSkill.skillData.dpsMultiplier = output.ProjectileCount
2284+
2285+
-- Calculate average damage scaling for sequential projectiles
2286+
-- Each projectile does more damage based on how many came before it
2287+
local moreDamagePerProj = skillData.KineticFusilladeSequentialDamage or 0
2288+
if moreDamagePerProj ~= 0 and output.ProjectileCount > 1 then
2289+
-- Average multiplier: sum of (0, X, 2X, 3X, ..., (n-1)X) / n
2290+
-- This equals: X * (0 + 1 + 2 + ... + (n-1)) / n = X * n(n-1)/2 / n = X * (n-1)/2
2291+
local avgMoreMult = moreDamagePerProj * (output.ProjectileCount - 1) / 2
2292+
activeSkill.skillModList:NewMod("Damage", "MORE", avgMoreMult, "Skill:KineticFusillade", ModFlag.Hit)
2293+
2294+
-- Store the average multiplier for display
2295+
output.KineticFusilladeAvgMoreMult = avgMoreMult
2296+
2297+
if breakdown then
2298+
local breakdownSequential = {}
2299+
t_insert(breakdownSequential, s_format("^8Each projectile deals^7 %d%%^8 more damage per previous projectile", moreDamagePerProj))
2300+
t_insert(breakdownSequential, s_format("^8With^7 %d^8 projectiles, damage progression is:^7", output.ProjectileCount))
2301+
for i = 1, output.ProjectileCount do
2302+
local projMult = moreDamagePerProj * (i - 1)
2303+
t_insert(breakdownSequential, s_format(" ^8Projectile %d:^7 %d%%^8 more damage", i, projMult))
2304+
end
2305+
t_insert(breakdownSequential, s_format("^8Average more multiplier:^7 %.1f%%", avgMoreMult))
2306+
breakdown.KineticFusilladeSequentialBreakdown = breakdownSequential
2307+
end
2308+
end
2309+
end
2310+
end,
2311+
postCritFunc = function(activeSkill, output, breakdown)
2312+
local skillData = activeSkill.skillData
2313+
local t_insert = table.insert
2314+
local s_format = string.format
2315+
2316+
local baseDelayBetweenProjectiles = 0.05
2317+
local projectileCount = 1
2318+
2319+
if activeSkill.skillPart == 1 then
2320+
projectileCount = output.ProjectileCount
2321+
end
2322+
2323+
-- Calculate effective attack rate accounting for delayed projectile firing
2324+
-- Projectiles orbit for base_skill_effect_duration before firing
2325+
-- Recasting resets the timer, so attacking too fast wastes potential damage
2326+
local baseDuration = skillData.duration
2327+
local actualDuration = output.Duration or baseDuration
2328+
local ticksNeededForInitialDelay = math.ceil(actualDuration / data.misc.ServerTickTime)
2329+
local timePerProjectile = baseDelayBetweenProjectiles * output.DurationMod
2330+
local timeForAllProjectiles = timePerProjectile * projectileCount
2331+
local effectiveDelay = ticksNeededForInitialDelay * data.misc.ServerTickTime + math.ceil(timeForAllProjectiles / data.misc.ServerTickTime) * data.misc.ServerTickTime
2332+
local maxEffectiveAPS = 1 / effectiveDelay
2333+
local currentAPS = output.Speed
2334+
2335+
output.KineticFusilladeMaxEffectiveAPS = maxEffectiveAPS
2336+
2337+
if breakdown then
2338+
local breakdownAPS = {}
2339+
t_insert(breakdownAPS, s_format("^1(These calculations are speculative and best-effort)", actualDuration))
2340+
t_insert(breakdownAPS, s_format("^8Delay of^7 %.3fs ^8before projectiles start firing", actualDuration))
2341+
t_insert(breakdownAPS, s_format("^8Each projectile fires sequentially with a^7 %.3fs ^8delay between each projectile", timePerProjectile))
2342+
t_insert(breakdownAPS, s_format("^8Server tick time:^7 %.3fs", data.misc.ServerTickTime))
2343+
t_insert(breakdownAPS, s_format("^8Ticks needed:^7 %d ^8(rounded up)", ticksNeededForInitialDelay + math.ceil(timeForAllProjectiles / data.misc.ServerTickTime)))
2344+
t_insert(breakdownAPS, s_format("^8Effective delay:^7 %.3fs", effectiveDelay))
2345+
t_insert(breakdownAPS, s_format("^8Max effective attack rate:^7 1 / %.3f = %.2f", effectiveDelay, maxEffectiveAPS))
2346+
if currentAPS and currentAPS > maxEffectiveAPS then
2347+
t_insert(breakdownAPS, "")
2348+
t_insert(breakdownAPS, s_format("^1Current attack rate (%.2f) exceeds max effective rate!", currentAPS))
2349+
t_insert(breakdownAPS, s_format("^1DPS is reduced by %.1f%%", (1 - maxEffectiveAPS / currentAPS) * 100))
2350+
elseif currentAPS then
2351+
t_insert(breakdownAPS, "")
2352+
t_insert(breakdownAPS, s_format("^2Current attack rate (%.2f) is within effective limits", currentAPS))
2353+
end
2354+
breakdown.KineticFusilladeMaxEffectiveAPS = breakdownAPS
2355+
end
2356+
2357+
-- Adjust dpsMultiplier if attacking too fast (only for "All Projectiles" mode)
2358+
if activeSkill.skillPart == 1 then
2359+
if currentAPS and currentAPS > maxEffectiveAPS then
2360+
local efficiencyRatio = maxEffectiveAPS / currentAPS
2361+
local originalMultiplier = skillData.dpsMultiplier or output.ProjectileCount
2362+
skillData.dpsMultiplier = originalMultiplier * efficiencyRatio
2363+
end
2364+
end
2365+
end,
2366+
statMap = {
2367+
["kinetic_fusillade_damage_+%_final_per_projectile_fired"] = {
2368+
skill("KineticFusilladeSequentialDamage", nil),
2369+
},
2370+
["quality_display_kinetic_fusillade_is_gem"] = {
2371+
},
2372+
},
22682373
#mods
22692374

22702375
#skill KineticRain

src/Modules/CalcSections.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,8 @@ return {
745745
{ label = "Normal Hits/Cast", haveOutput = "NormalHitsPerCast", { format = "{3:output:NormalHitsPerCast}", { breakdown = "NormalHitsPerCast" }, }, },
746746
{ label = "Super Hits/Cast", haveOutput = "SuperchargedHitsPerCast", { format = "{3:output:SuperchargedHitsPerCast}", { breakdown = "SuperchargedHitsPerCast" }, }, },
747747
{ label = "DPS Multiplier", haveOutput = "SkillDPSMultiplier", { format = "{3:output:SkillDPSMultiplier}", { breakdown = "SkillDPSMultiplier" }, }, },
748+
{ label = "Average Seq More", haveOutput = "KineticFusilladeAvgMoreMult", { format = "{1:output:KineticFusilladeAvgMoreMult}%", { breakdown = "KineticFusilladeSequentialBreakdown" }, }, },
749+
{ label = "Max Effective APS", haveOutput = "KineticFusilladeMaxEffectiveAPS", { format = "{2:output:KineticFusilladeMaxEffectiveAPS}", { breakdown = "KineticFusilladeMaxEffectiveAPS" }, }, },
748750
-- Traps
749751
{ label = "Avg. Active Traps", haveOutput = "AverageActiveTraps", { format = "{2:output:AverageActiveTraps}", { breakdown = "AverageActiveTraps" }, }, },
750752
{ label = "Active Trap Limit", flag = "trap", { format = "{0:output:ActiveTrapLimit}", { modName = "ActiveTrapLimit", cfg = "skill" }, }, },

0 commit comments

Comments
 (0)