diff --git a/src/Data/Skills/act_int.lua b/src/Data/Skills/act_int.lua index 547fe1e017..ba2bb7b110 100644 --- a/src/Data/Skills/act_int.lua +++ b/src/Data/Skills/act_int.lua @@ -6130,6 +6130,115 @@ skills["EmberFusilladePlayer"] = { [39] = { critChance = 7, levelRequirement = 90, cost = { Mana = 206, }, }, [40] = { critChance = 7, levelRequirement = 90, cost = { Mana = 225, }, }, }, + 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.damagePerProjectile 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:EmberFusilladePlayer") + + -- Store the average multiplier for display + output.EmberFusilladeAvgMoreMult = 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.EmberFusilladeSequentialBreakdown = breakdownSequential + breakdown.ProjectileCount = breakdown.ProjectileCount or {} + t_insert(breakdown.ProjectileCount, s_format("^8Maximum number of Ember Fusillade projectiles:^7 %d", activeSkill.skillModList:Override(activeSkill.skillCfg, "ProjectileCountMaximum"))) + 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 = skillData.delayPerProjectile + 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 + -- Formula: totalTime = (hoverDelay + delayBetweenProj * nProj) * durationMod + local ge = activeSkill.activeEffect.grantedEffect + local stats = calcLib.buildSkillInstanceStats(activeSkill.activeEffect, ge, ge.statSets[1]) + local hoverDelay = (stats.base_skill_effect_duration) / 1000 + local durationMod = output.DurationMod + local baseTimeForAllProjectiles = baseDelayBetweenProjectiles * (projectileCount - 1) + local effectiveDelay = (hoverDelay) * durationMod + baseTimeForAllProjectiles + local effectiveDelayRounded = math.ceil(effectiveDelay / data.misc.ServerTickTime) * data.misc.ServerTickTime + local maxEffectiveAPS = 1 / effectiveDelayRounded + local currentAPS = output.Speed + + output.EmberFusilladeMaxEffectiveAPS = maxEffectiveAPS + + if breakdown then + local breakdownAPS = {} + t_insert(breakdownAPS, s_format("^8Base hover delay:^7 %.1fs", hoverDelay)) + t_insert(breakdownAPS, s_format("^8Base delay between projectiles:^7 %.2fs", baseDelayBetweenProjectiles)) + t_insert(breakdownAPS, s_format("^8Base time for^7 %d added ^8projectiles:^7 %.2fs x %d = %.1fs", (projectileCount - 1), baseDelayBetweenProjectiles, (projectileCount - 1), baseTimeForAllProjectiles)) + t_insert(breakdownAPS, s_format("^8Duration modifier:^7 %.4f", durationMod)) + t_insert(breakdownAPS, "") + t_insert(breakdownAPS, s_format("^6Lockstep")) + t_insert(breakdownAPS, s_format("^8Effective delay:^7 (%.1f + %.2f) x %.4f = %.4fs", hoverDelay, baseTimeForAllProjectiles, durationMod, effectiveDelay)) + t_insert(breakdownAPS, s_format("^8Rounded up to nearest server tick^7 = %.4fs", effectiveDelayRounded)) + t_insert(breakdownAPS, s_format("^8Max effective attack rate:^7 1 / %.4f = %.2f", effectiveDelayRounded, maxEffectiveAPS)) + if currentAPS then + local currentInc = activeSkill.skillModList:Sum("INC", activeSkill.skillCfg, "Speed") + local neededInc = ((maxEffectiveAPS / currentAPS) * (1 + currentInc / 100) - 1) * 100 + local additionalInc = neededInc - currentInc + if currentAPS > maxEffectiveAPS then + t_insert(breakdownAPS, s_format("^1Current attack rate (%.2f) exceeds max effective rate!", currentAPS)) + t_insert(breakdownAPS, s_format("^1Reduce attack speed by at least^7 %d%% ^1", math.ceil(-additionalInc))) + t_insert(breakdownAPS, s_format("^1DPS is reduced by %.1f%%", (1 - maxEffectiveAPS / currentAPS) * 100)) + else + t_insert(breakdownAPS, s_format("^2Current attack rate (%.2f) is within effective limits", currentAPS)) + t_insert(breakdownAPS, s_format("^2You can add up to^7 %d%% ^2increased attack speed", math.floor(additionalInc))) + end + end + breakdown.EmberFusilladeMaxEffectiveAPS = 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, statSets = { [1] = { label = "Projectile", @@ -6137,6 +6246,21 @@ skills["EmberFusilladePlayer"] = { incrementalEffectiveness = 0.12999999523163, damageIncrementalEffectiveness = 0.0082000000402331, statDescriptionScope = "ember_fusillade_statset_0", + statMap = { + ["ember_fusillade_damage_+%_final_per_ember_fired"] = { + skill("damagePerProjectile", nil), + }, + ["blazing_cluster_delay_between_projectiles_ms"] = { + skill("delayPerProjectile", nil), + div = 1000, + }, + ["quality_display_ember_fusilade_is_gem"] = { + -- Display only + }, + ["blazing_cluster_maximum_number_of_projectiles_allowed"] = { + mod("ProjectileCountMaximum", "OVERRIDE", nil), + }, + }, baseFlags = { spell = true, duration = true, @@ -6215,6 +6339,21 @@ skills["EmberFusilladePlayer"] = { incrementalEffectiveness = 0.054999999701977, damageIncrementalEffectiveness = 0.0082000000402331, statDescriptionScope = "ember_fusillade_statset_1", + statMap = { + ["ember_fusillade_damage_+%_final_per_ember_fired"] = { + skill("damagePerProjectile", nil), + }, + ["blazing_cluster_delay_between_projectiles_ms"] = { + skill("delayPerProjectile", nil), + div = 1000, + }, + ["quality_display_ember_fusilade_is_gem"] = { + -- Display only + }, + ["blazing_cluster_maximum_number_of_projectiles_allowed"] = { + mod("ProjectileCountMaximum", "OVERRIDE", nil), + }, + }, baseFlags = { spell = true, area = true, @@ -6294,6 +6433,21 @@ skills["EmberFusilladePlayer"] = { incrementalEffectiveness = 0.12999999523163, damageIncrementalEffectiveness = 0.0082000000402331, statDescriptionScope = "ember_fusillade_statset_2", + statMap = { + ["ember_fusillade_damage_+%_final_per_ember_fired"] = { + skill("damagePerProjectile", nil), + }, + ["blazing_cluster_delay_between_projectiles_ms"] = { + skill("delayPerProjectile", nil), + div = 1000, + }, + ["quality_display_ember_fusilade_is_gem"] = { + -- Display only + }, + ["blazing_cluster_maximum_number_of_projectiles_allowed"] = { + mod("ProjectileCountMaximum", "OVERRIDE", nil), + }, + }, baseFlags = { spell = true, projectile = true, diff --git a/src/Data/Skills/other.lua b/src/Data/Skills/other.lua index 3edbbbd09b..f7ba893aaa 100644 --- a/src/Data/Skills/other.lua +++ b/src/Data/Skills/other.lua @@ -5776,7 +5776,7 @@ skills["EncaseInJadePlayer"] = { } skills["SupportEshsProwess"] = { name = "Esh's Prowess", - description = "Supports Lightning skills that Hit targets, granting them an additional level and causing them to only roll the minimum or maximum value for Lightning damage. Does not support skills which do not have levels.", + description = "Supports Lightning Skills that Hit, granting them an additional level and causing them to only roll the minimum or maximum value for Lightning damage. Does not Support Skills which do not have levels.", color = 4, support = true, requireSkillTypes = { SkillType.Lightning, SkillType.Damage, }, @@ -17787,7 +17787,7 @@ skills["StarbornOnslaughtPlayer"] = { } skills["SupportStyrnsFerocity"] = { name = "Styrn's Ferocity", - description = "Supports Shield Skills that Hit enemies, causing those Hits to Crush their targets, additionally causing any Physical Damage Hits against those targets while Crushed to Break their Armour.", + description = "Supports Shield Skills that Hit, causing those Hits to Crush their targets, and Physical Hits against those targets while Crushed to Break their Armour.", color = 4, support = true, requireSkillTypes = { SkillType.Attack, SkillType.Damage, SkillType.OR, SkillType.RequiresShield, SkillType.AND, }, diff --git a/src/Export/Skills/act_int.txt b/src/Export/Skills/act_int.txt index a8fea4a3bb..2d1f85f49a 100644 --- a/src/Export/Skills/act_int.txt +++ b/src/Export/Skills/act_int.txt @@ -439,14 +439,168 @@ statMap = { #skillEnd #skill EmberFusilladePlayer +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.damagePerProjectile 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:EmberFusilladePlayer") + + -- Store the average multiplier for display + output.EmberFusilladeAvgMoreMult = 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.EmberFusilladeSequentialBreakdown = breakdownSequential + breakdown.ProjectileCount = breakdown.ProjectileCount or {} + t_insert(breakdown.ProjectileCount, s_format("^8Maximum number of Ember Fusillade projectiles:^7 %d", activeSkill.skillModList:Override(activeSkill.skillCfg, "ProjectileCountMaximum"))) + 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 = skillData.delayPerProjectile + 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 + -- Formula: totalTime = (hoverDelay + delayBetweenProj * nProj) * durationMod + local ge = activeSkill.activeEffect.grantedEffect + local stats = calcLib.buildSkillInstanceStats(activeSkill.activeEffect, ge, ge.statSets[1]) + local hoverDelay = (stats.base_skill_effect_duration) / 1000 + local durationMod = output.DurationMod + local baseTimeForAllProjectiles = baseDelayBetweenProjectiles * (projectileCount - 1) + local effectiveDelay = (hoverDelay) * durationMod + baseTimeForAllProjectiles + local effectiveDelayRounded = math.ceil(effectiveDelay / data.misc.ServerTickTime) * data.misc.ServerTickTime + local maxEffectiveAPS = 1 / effectiveDelayRounded + local currentAPS = output.Speed + + output.EmberFusilladeMaxEffectiveAPS = maxEffectiveAPS + + if breakdown then + local breakdownAPS = {} + t_insert(breakdownAPS, s_format("^8Base hover delay:^7 %.1fs", hoverDelay)) + t_insert(breakdownAPS, s_format("^8Base delay between projectiles:^7 %.2fs", baseDelayBetweenProjectiles)) + t_insert(breakdownAPS, s_format("^8Base time for^7 %d added ^8projectiles:^7 %.2fs x %d = %.1fs", (projectileCount - 1), baseDelayBetweenProjectiles, (projectileCount - 1), baseTimeForAllProjectiles)) + t_insert(breakdownAPS, s_format("^8Duration modifier:^7 %.4f", durationMod)) + t_insert(breakdownAPS, "") + t_insert(breakdownAPS, s_format("^6Lockstep")) + t_insert(breakdownAPS, s_format("^8Effective delay:^7 (%.1f + %.2f) x %.4f = %.4fs", hoverDelay, baseTimeForAllProjectiles, durationMod, effectiveDelay)) + t_insert(breakdownAPS, s_format("^8Rounded up to nearest server tick^7 = %.4fs", effectiveDelayRounded)) + t_insert(breakdownAPS, s_format("^8Max effective attack rate:^7 1 / %.4f = %.2f", effectiveDelayRounded, maxEffectiveAPS)) + if currentAPS then + local currentInc = activeSkill.skillModList:Sum("INC", activeSkill.skillCfg, "Speed") + local neededInc = ((maxEffectiveAPS / currentAPS) * (1 + currentInc / 100) - 1) * 100 + local additionalInc = neededInc - currentInc + if currentAPS > maxEffectiveAPS then + t_insert(breakdownAPS, s_format("^1Current attack rate (%.2f) exceeds max effective rate!", currentAPS)) + t_insert(breakdownAPS, s_format("^1Reduce attack speed by at least^7 %d%% ^1", math.ceil(-additionalInc))) + t_insert(breakdownAPS, s_format("^1DPS is reduced by %.1f%%", (1 - maxEffectiveAPS / currentAPS) * 100)) + else + t_insert(breakdownAPS, s_format("^2Current attack rate (%.2f) is within effective limits", currentAPS)) + t_insert(breakdownAPS, s_format("^2You can add up to^7 %d%% ^2increased attack speed", math.floor(additionalInc))) + end + end + breakdown.EmberFusilladeMaxEffectiveAPS = 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, #set EmberFusilladePlayer #flags spell duration projectile +statMap = { + ["ember_fusillade_damage_+%_final_per_ember_fired"] = { + skill("damagePerProjectile", nil), + }, + ["blazing_cluster_delay_between_projectiles_ms"] = { + skill("delayPerProjectile", nil), + div = 1000, + }, + ["quality_display_ember_fusilade_is_gem"] = { + -- Display only + }, + ["blazing_cluster_maximum_number_of_projectiles_allowed"] = { + mod("ProjectileCountMaximum", "OVERRIDE", nil), + }, +}, #mods #set EmberFusilladeExplosionPlayer #flags spell area projectile +statMap = { + ["ember_fusillade_damage_+%_final_per_ember_fired"] = { + skill("damagePerProjectile", nil), + }, + ["blazing_cluster_delay_between_projectiles_ms"] = { + skill("delayPerProjectile", nil), + div = 1000, + }, + ["quality_display_ember_fusilade_is_gem"] = { + -- Display only + }, + ["blazing_cluster_maximum_number_of_projectiles_allowed"] = { + mod("ProjectileCountMaximum", "OVERRIDE", nil), + }, +}, #mods #set EmberFusilladeBeamPlayer #flags spell projectile +statMap = { + ["ember_fusillade_damage_+%_final_per_ember_fired"] = { + skill("damagePerProjectile", nil), + }, + ["blazing_cluster_delay_between_projectiles_ms"] = { + skill("delayPerProjectile", nil), + div = 1000, + }, + ["quality_display_ember_fusilade_is_gem"] = { + -- Display only + }, + ["blazing_cluster_maximum_number_of_projectiles_allowed"] = { + mod("ProjectileCountMaximum", "OVERRIDE", nil), + }, +}, #mods #skillEnd diff --git a/src/Modules/BuildDisplayStats.lua b/src/Modules/BuildDisplayStats.lua index 1547d24501..138571ba70 100644 --- a/src/Modules/BuildDisplayStats.lua +++ b/src/Modules/BuildDisplayStats.lua @@ -51,6 +51,7 @@ local displayStats = { { stat = "Speed", label = "Attack Rate", fmt = ".2f", compPercent = true, flag = "attack", condFunc = function(v, o) return v > 0 and not o.ChannelTime and (o.TriggerTime or 0) == 0 end, breakdown = "Speed", modNames = { "Speed" }, ignoredSections = { "Inc. Cast Speed", "More Cast Speed", } }, { stat = "Speed", label = "Cast Rate", fmt = ".2f", compPercent = true, flag = "spell", condFunc = function(v, o) return v > 0 and not o.ChannelTime and (o.TriggerTime or 0) == 0 end, modNames = { "Speed" } }, { stat = "Speed", label = "Effective Trigger Rate", fmt = ".2f", compPercent = true, condFunc = function(v,o) return (o.TriggerTime or 0) ~= 0 end }, + { stat = "EmberFusilladeMaxEffectiveAPS", label = "Max Eff. EF Cast Rate", fmt = ".2f", compPercent = true, condFunc = function(v) return v > 0 end }, { stat = "WarcryCastTime", label = "Cast Time", fmt = ".2fs", compPercent = true, lowerIsBetter = true, flag = "warcry" }, { stat = "ChannelTime", label = "Channel Time", fmt = ".2fs", compPercent = true, flag = "channelRelease", lowerIsBetter = true, condFunc = function(v,o) return not o.TriggerTime end }, { stat = "HitSpeed", label = "Hit Rate", fmt = ".2f", compPercent = true, condFunc = function(v,o) return not o.TriggerTime end }, diff --git a/src/Modules/CalcOffence.lua b/src/Modules/CalcOffence.lua index 960a5e16d4..a39da9f0e1 100644 --- a/src/Modules/CalcOffence.lua +++ b/src/Modules/CalcOffence.lua @@ -1448,9 +1448,10 @@ function calcs.offence(env, actor, activeSkill) if skillModList:Flag(skillCfg, "NoAdditionalProjectiles") then output.ProjectileCount = 1 else + local projMax = skillModList:Override(skillCfg, "ProjectileCountMaximum") or m_huge local projBase = skillModList:Sum("BASE", skillCfg, "ProjectileCount") + 2 * skillModList:Sum("BASE", skillCfg, "TwoAdditionalProjectilesChance") / 100 + skillModList:Sum("BASE", skillCfg, "SurpassingProjectileChance") / 100 local projMore = skillModList:More(skillCfg, "ProjectileCount") - output.ProjectileCount = projBase * projMore + output.ProjectileCount = m_min(projBase * projMore, projMax) end if skillModList:Flag(skillCfg, "AdditionalProjectilesAddBouncesInstead") then local projBase = skillModList:Sum("BASE", skillCfg, "ProjectileCount") + 2 * skillModList:Sum("BASE", skillCfg, "TwoAdditionalProjectilesChance") / 100 + skillModList:Sum("BASE", skillCfg, "SurpassingProjectileChance") / 100 + skillModList:Sum("BASE", skillCfg, "BounceCount") - 1 @@ -3976,6 +3977,7 @@ function calcs.offence(env, actor, activeSkill) end end end + runSkillFunc("postCritFunc") if not output.CritEffect then if skillModList:Flag(cfg, "NoCritMultiplier") then output.CritMultiplier = 1 diff --git a/src/Modules/CalcSections.lua b/src/Modules/CalcSections.lua index e5604958c8..2555492e5c 100644 --- a/src/Modules/CalcSections.lua +++ b/src/Modules/CalcSections.lua @@ -882,6 +882,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 = "EmberFusilladeAvgMoreMult", { format = "{1:output:EmberFusilladeAvgMoreMult}%", { breakdown = "EmberFusilladeSequentialBreakdown" }, }, }, + { label = "Max Effective APS", haveOutput = "EmberFusilladeMaxEffectiveAPS", { format = "{2:output:EmberFusilladeMaxEffectiveAPS}", { breakdown = "EmberFusilladeMaxEffectiveAPS" }, }, }, -- 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" }, }, },