diff --git a/src/Export/Skills/act_int.txt b/src/Export/Skills/act_int.txt index 1a2be75344..493a122736 100644 --- a/src/Export/Skills/act_int.txt +++ b/src/Export/Skills/act_int.txt @@ -2265,6 +2265,111 @@ local skills, mod, flag, skill = ... #skill KineticFusillade #flags attack projectile area duration + parts = { + { + name = "All Projectiles", + }, + { + name = "1 Projectile" + }, + }, + preDamageFunc = function(activeSkill, output, breakdown) + local skillData = activeSkill.skillData + local t_insert = table.insert + local s_format = string.format + + if activeSkill.skillPart == 1 then + -- Set base dpsMultiplier for projectile count + activeSkill.skillData.dpsMultiplier = output.ProjectileCount + + -- Calculate average damage scaling for sequential projectiles + -- Each projectile does more damage based on how many came before it + local moreDamagePerProj = skillData.KineticFusilladeSequentialDamage or 0 + if moreDamagePerProj ~= 0 and output.ProjectileCount > 1 then + -- Average multiplier: sum of (0, X, 2X, 3X, ..., (n-1)X) / n + -- This equals: X * (0 + 1 + 2 + ... + (n-1)) / n = X * n(n-1)/2 / n = X * (n-1)/2 + local avgMoreMult = moreDamagePerProj * (output.ProjectileCount - 1) / 2 + activeSkill.skillModList:NewMod("Damage", "MORE", avgMoreMult, "Skill:KineticFusillade", ModFlag.Hit) + + -- Store the average multiplier for display + output.KineticFusilladeAvgMoreMult = avgMoreMult + + if breakdown then + local breakdownSequential = {} + t_insert(breakdownSequential, s_format("^8Each projectile deals^7 %d%%^8 more damage per previous projectile", moreDamagePerProj)) + t_insert(breakdownSequential, s_format("^8With^7 %d^8 projectiles, damage progression is:^7", output.ProjectileCount)) + for i = 1, output.ProjectileCount do + local projMult = moreDamagePerProj * (i - 1) + t_insert(breakdownSequential, s_format(" ^8Projectile %d:^7 %d%%^8 more damage", i, projMult)) + end + t_insert(breakdownSequential, s_format("^8Average more multiplier:^7 %.1f%%", avgMoreMult)) + breakdown.KineticFusilladeSequentialBreakdown = breakdownSequential + end + end + end + end, + postCritFunc = function(activeSkill, output, breakdown) + local skillData = activeSkill.skillData + local t_insert = table.insert + local s_format = string.format + + local baseDelayBetweenProjectiles = 0.05 + local projectileCount = 1 + + if activeSkill.skillPart == 1 then + projectileCount = output.ProjectileCount + end + + -- Calculate effective attack rate accounting for delayed projectile firing + -- Projectiles orbit for base_skill_effect_duration before firing + -- Recasting resets the timer, so attacking too fast wastes potential damage + local baseDuration = skillData.duration + local actualDuration = output.Duration or baseDuration + local ticksNeededForInitialDelay = math.ceil(actualDuration / data.misc.ServerTickTime) + local timePerProjectile = baseDelayBetweenProjectiles * output.DurationMod + local timeForAllProjectiles = timePerProjectile * projectileCount + local effectiveDelay = ticksNeededForInitialDelay * data.misc.ServerTickTime + math.ceil(timeForAllProjectiles / data.misc.ServerTickTime) * data.misc.ServerTickTime + local maxEffectiveAPS = 1 / effectiveDelay + local currentAPS = output.Speed + + output.KineticFusilladeMaxEffectiveAPS = maxEffectiveAPS + + if breakdown then + local breakdownAPS = {} + t_insert(breakdownAPS, s_format("^1(These calculations are speculative and best-effort)", actualDuration)) + t_insert(breakdownAPS, s_format("^8Delay of^7 %.3fs ^8before projectiles start firing", actualDuration)) + t_insert(breakdownAPS, s_format("^8Each projectile fires sequentially with a^7 %.3fs ^8delay between each projectile", timePerProjectile)) + t_insert(breakdownAPS, s_format("^8Server tick time:^7 %.3fs", data.misc.ServerTickTime)) + t_insert(breakdownAPS, s_format("^8Ticks needed:^7 %d ^8(rounded up)", ticksNeededForInitialDelay + math.ceil(timeForAllProjectiles / data.misc.ServerTickTime))) + t_insert(breakdownAPS, s_format("^8Effective delay:^7 %.3fs", effectiveDelay)) + t_insert(breakdownAPS, s_format("^8Max effective attack rate:^7 1 / %.3f = %.2f", effectiveDelay, maxEffectiveAPS)) + if currentAPS and currentAPS > maxEffectiveAPS then + t_insert(breakdownAPS, "") + t_insert(breakdownAPS, s_format("^1Current attack rate (%.2f) exceeds max effective rate!", currentAPS)) + t_insert(breakdownAPS, s_format("^1DPS is reduced by %.1f%%", (1 - maxEffectiveAPS / currentAPS) * 100)) + elseif currentAPS then + t_insert(breakdownAPS, "") + t_insert(breakdownAPS, s_format("^2Current attack rate (%.2f) is within effective limits", currentAPS)) + end + breakdown.KineticFusilladeMaxEffectiveAPS = breakdownAPS + end + + -- Adjust dpsMultiplier if attacking too fast (only for "All Projectiles" mode) + if activeSkill.skillPart == 1 then + if currentAPS and currentAPS > maxEffectiveAPS then + local efficiencyRatio = maxEffectiveAPS / currentAPS + local originalMultiplier = skillData.dpsMultiplier or output.ProjectileCount + skillData.dpsMultiplier = originalMultiplier * efficiencyRatio + end + end + end, + statMap = { + ["kinetic_fusillade_damage_+%_final_per_projectile_fired"] = { + skill("KineticFusilladeSequentialDamage", nil), + }, + ["quality_display_kinetic_fusillade_is_gem"] = { + }, + }, #mods #skill KineticRain diff --git a/src/Modules/CalcSections.lua b/src/Modules/CalcSections.lua index d16163a3e8..37a994fc1a 100644 --- a/src/Modules/CalcSections.lua +++ b/src/Modules/CalcSections.lua @@ -737,6 +737,8 @@ return { { label = "Normal Hits/Cast", haveOutput = "NormalHitsPerCast", { format = "{3:output:NormalHitsPerCast}", { breakdown = "NormalHitsPerCast" }, }, }, { label = "Super Hits/Cast", haveOutput = "SuperchargedHitsPerCast", { format = "{3:output:SuperchargedHitsPerCast}", { breakdown = "SuperchargedHitsPerCast" }, }, }, { label = "DPS Multiplier", haveOutput = "SkillDPSMultiplier", { format = "{3:output:SkillDPSMultiplier}", { breakdown = "SkillDPSMultiplier" }, }, }, + { label = "Average Seq More", haveOutput = "KineticFusilladeAvgMoreMult", { format = "{1:output:KineticFusilladeAvgMoreMult}%", { breakdown = "KineticFusilladeSequentialBreakdown" }, }, }, + { label = "Max Effective APS", haveOutput = "KineticFusilladeMaxEffectiveAPS", { format = "{2:output:KineticFusilladeMaxEffectiveAPS}", { breakdown = "KineticFusilladeMaxEffectiveAPS" }, }, }, -- Traps { label = "Avg. Active Traps", haveOutput = "AverageActiveTraps", { format = "{2:output:AverageActiveTraps}", { breakdown = "AverageActiveTraps" }, }, }, { label = "Active Trap Limit", flag = "trap", { format = "{0:output:ActiveTrapLimit}", { modName = "ActiveTrapLimit", cfg = "skill" }, }, },