Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions src/Data/Skills/act_int.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6130,13 +6130,137 @@ 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",
baseEffectiveness = 2.0999999046326,
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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/Data/Skills/other.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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, },
Expand Down Expand Up @@ -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, },
Expand Down
154 changes: 154 additions & 0 deletions src/Export/Skills/act_int.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions src/Modules/BuildDisplayStats.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
4 changes: 3 additions & 1 deletion src/Modules/CalcOffence.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading