@@ -11142,9 +11142,117 @@ skills["KineticFusillade"] = {
1114211142 },
1114311143 statDescriptionScope = "skill_stat_descriptions",
1114411144 castTime = 1,
11145+ parts = {
11146+ {
11147+ name = "All Projectiles",
11148+ },
11149+ {
11150+ name = "1 Projectile"
11151+ },
11152+ },
11153+ preDamageFunc = function(activeSkill, output, breakdown)
11154+ local skillData = activeSkill.skillData
11155+ local t_insert = table.insert
11156+ local s_format = string.format
11157+
11158+ if activeSkill.skillPart == 1 then
11159+ -- Set base dpsMultiplier for projectile count
11160+ activeSkill.skillData.dpsMultiplier = output.ProjectileCount
11161+
11162+ -- Calculate average damage scaling for sequential projectiles
11163+ -- Each projectile does more damage based on how many came before it
11164+ local moreDamagePerProj = skillData.damagePerProjectile or 0
11165+ if moreDamagePerProj ~= 0 and output.ProjectileCount > 1 then
11166+ -- Average multiplier: sum of (0, X, 2X, 3X, ..., (n-1)X) / n
11167+ -- This equals: X * (0 + 1 + 2 + ... + (n-1)) / n = X * n(n-1)/2 / n = X * (n-1)/2
11168+ local avgMoreMult = moreDamagePerProj * (output.ProjectileCount - 1) / 2
11169+ activeSkill.skillModList:NewMod("Damage", "MORE", avgMoreMult, "Skill:KineticFusillade", ModFlag.Hit)
11170+
11171+ -- Store the average multiplier for display
11172+ output.KineticFusilladeAvgMoreMult = avgMoreMult
11173+
11174+ if breakdown then
11175+ local breakdownSequential = {}
11176+ t_insert(breakdownSequential, s_format("^8Each projectile deals^7 %d%%^8 more damage per previous projectile", moreDamagePerProj))
11177+ t_insert(breakdownSequential, s_format("^8With^7 %d^8 projectiles, damage progression is:^7", output.ProjectileCount))
11178+ for i = 1, output.ProjectileCount do
11179+ local projMult = moreDamagePerProj * (i - 1)
11180+ t_insert(breakdownSequential, s_format(" ^8Projectile %d:^7 %d%%^8 more damage", i, projMult))
11181+ end
11182+ t_insert(breakdownSequential, s_format("^8Average more multiplier:^7 %.1f%%", avgMoreMult))
11183+ breakdown.KineticFusilladeSequentialBreakdown = breakdownSequential
11184+ end
11185+ end
11186+ end
11187+ end,
11188+ postCritFunc = function(activeSkill, output, breakdown)
11189+ local skillData = activeSkill.skillData
11190+ local t_insert = table.insert
11191+ local s_format = string.format
11192+
11193+ local baseDelayBetweenProjectiles = skillData.delayPerProjectile
11194+ local projectileCount = 1
11195+
11196+ if activeSkill.skillPart == 1 then
11197+ projectileCount = output.ProjectileCount
11198+ end
11199+
11200+ -- Calculate effective attack rate accounting for delayed projectile firing
11201+ -- Projectiles orbit for base_skill_effect_duration before firing
11202+ -- Recasting resets the timer, so attacking too fast wastes potential damage
11203+ local baseDuration = skillData.duration
11204+ local actualDuration = output.Duration or baseDuration
11205+ local ticksNeededForInitialDelay = math.ceil(actualDuration / data.misc.ServerTickTime)
11206+ local timePerProjectile = baseDelayBetweenProjectiles * output.DurationMod
11207+ local timeForAllProjectiles = timePerProjectile * projectileCount
11208+ local effectiveDelay = ticksNeededForInitialDelay * data.misc.ServerTickTime + math.ceil(timeForAllProjectiles / data.misc.ServerTickTime) * data.misc.ServerTickTime
11209+ local maxEffectiveAPS = 1 / effectiveDelay
11210+ local currentAPS = output.Speed
11211+
11212+ output.KineticFusilladeMaxEffectiveAPS = maxEffectiveAPS
11213+
11214+ if breakdown then
11215+ local breakdownAPS = {}
11216+ t_insert(breakdownAPS, s_format("^1(These calculations are speculative and best-effort)", actualDuration))
11217+ t_insert(breakdownAPS, s_format("^8Delay of^7 %.3fs ^8before projectiles start firing", actualDuration))
11218+ t_insert(breakdownAPS, s_format("^8Each projectile fires sequentially with a^7 %.3fs ^8delay between each projectile", timePerProjectile))
11219+ t_insert(breakdownAPS, s_format("^8Server tick time:^7 %.3fs", data.misc.ServerTickTime))
11220+ t_insert(breakdownAPS, s_format("^8Ticks needed:^7 %d ^8(rounded up)", ticksNeededForInitialDelay + math.ceil(timeForAllProjectiles / data.misc.ServerTickTime)))
11221+ t_insert(breakdownAPS, s_format("^8Effective delay:^7 %.3fs", effectiveDelay))
11222+ t_insert(breakdownAPS, s_format("^8Max effective attack rate:^7 1 / %.3f = %.2f", effectiveDelay, maxEffectiveAPS))
11223+ if currentAPS and currentAPS > maxEffectiveAPS then
11224+ t_insert(breakdownAPS, "")
11225+ t_insert(breakdownAPS, s_format("^1Current attack rate (%.2f) exceeds max effective rate!", currentAPS))
11226+ t_insert(breakdownAPS, s_format("^1DPS is reduced by %.1f%%", (1 - maxEffectiveAPS / currentAPS) * 100))
11227+ elseif currentAPS then
11228+ t_insert(breakdownAPS, "")
11229+ t_insert(breakdownAPS, s_format("^2Current attack rate (%.2f) is within effective limits", currentAPS))
11230+ end
11231+ breakdown.KineticFusilladeMaxEffectiveAPS = breakdownAPS
11232+ end
11233+
11234+ -- Adjust dpsMultiplier if attacking too fast (only for "All Projectiles" mode)
11235+ if activeSkill.skillPart == 1 then
11236+ if currentAPS and currentAPS > maxEffectiveAPS then
11237+ local efficiencyRatio = maxEffectiveAPS / currentAPS
11238+ local originalMultiplier = skillData.dpsMultiplier or output.ProjectileCount
11239+ skillData.dpsMultiplier = originalMultiplier * efficiencyRatio
11240+ end
11241+ end
11242+ end,
11243+ statMap = {
11244+ ["kinetic_fusillade_damage_+%_final_per_projectile_fired"] = {
11245+ skill("damagePerProjectile", nil),
11246+ },
11247+ ["kinetic_fusillade_base_delay_between_projectiles_ms+%_final_per_projectile_fired"] = {
11248+ skill("delayPerProjectile", nil),
11249+ div = 1000,
11250+ },
11251+ ["quality_display_kinetic_fusillade_is_gem"] = {
11252+ },
11253+ },
1114511254 baseFlags = {
1114611255 attack = true,
11147- projectile = true,
1114811256 area = true,
1114911257 duration = true,
1115011258 },
0 commit comments