Skip to content
Open
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
75 changes: 75 additions & 0 deletions spec/System/TestSkills_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,81 @@ describe("TestAttacks", function()
assert.True(preAdrenalineMaxStages < build.calcsTab.mainEnv.player.activeSkillList[1].skillModList:Sum("BASE", nil, "Multiplier:BlightMaxStages"))
end)

it("calculates Wintertide Brand average damage for attached brands and Wintertide's End", function()
local function getAverageDamageMultiplier()
for _, mod in ipairs(build.calcsTab.mainEnv.player.mainSkill.skillModList) do
if mod.source == "Wintertide Brand Average Multiplier" then
return mod.value
end
end
end

build.skillsTab:PasteSocketGroup("Wintertide Brand 20/0 1\n")
runCallback("OnFrame")
local mainSocketGroup = build.skillsTab.socketGroupList[build.mainSocketGroup]
local srcInstance = mainSocketGroup.displaySkillList[mainSocketGroup.mainActiveSkill].activeEffect.srcInstance
srcInstance.skillPart = 1
build.modFlag = true
build.buildFlag = true
runCallback("OnFrame")

assert.is_true(build.configTab.varControls.BrandsAttachedToEnemy.shown())
assert.are.equals(8, build.calcsTab.mainOutput.BrandTicks)
assert.are.near(2, build.calcsTab.mainOutput.DurationTertiary, 0.02)
assert.are.equals(20, build.calcsTab.mainEnv.player.mainSkill.skillModList:Sum("BASE", build.calcsTab.mainEnv.player.mainSkill.skillCfg, "Multiplier:WintertideBrandMaxStages"))
assert.are.equals("Average Damage for 2 attached Brands", build.calcsTab.mainEnv.player.mainSkill.infoMessage)
assert.are.near(500, getAverageDamageMultiplier(), 10 ^ -9)

build.configTab.input.BrandsAttachedToEnemy = 1
build.configTab:BuildModList()
runCallback("OnFrame")

assert.are.equals("Average Damage for 1 attached Brand", build.calcsTab.mainEnv.player.mainSkill.infoMessage)
assert.are.near(330, getAverageDamageMultiplier(), 10 ^ -9)

build.configTab.input.BrandsAttachedToEnemy = nil
build.configTab.input.customMods = "You can have an additional Brand Attached to an Enemy"
build.configTab:BuildModList()
runCallback("OnFrame")

assert.are.equals("Average Damage for 3 attached Brands", build.calcsTab.mainEnv.player.mainSkill.infoMessage)
assert.are.near(670, getAverageDamageMultiplier(), 10 ^ -9)

build.configTab.input.customMods = "400% increased Skill Effect Duration"
build.configTab:BuildModList()
runCallback("OnFrame")

assert.are.equals(40, build.calcsTab.mainOutput.BrandTicks)
assert.are.near(1190, getAverageDamageMultiplier(), 10 ^ -9)
end)

it("multiplies Brand DPS by the attached Brand count", function()
build.skillsTab:PasteSocketGroup("Armageddon Brand 20/0 1\n")
runCallback("OnFrame")

local singleBrandDPS = build.calcsTab.mainOutput.TotalDPS
build.configTab.input.customMods = "You can have an additional Brand Attached to an Enemy"
build.configTab:BuildModList()
runCallback("OnFrame")

assert.are.near(singleBrandDPS * 2, build.calcsTab.mainOutput.TotalDPS, 10 ^ -9)
assert.are.equals("DPS for 2 attached Brands", build.calcsTab.mainEnv.player.mainSkill.infoMessage)
end)

it("multiplies non-stacking Brand damage over time by the attached Brand count", function()
build.skillsTab:PasteSocketGroup("Wintertide Brand 20/0 1\n")
build.configTab.input.BrandsAttachedToEnemy = 1
build.configTab:BuildModList()
runCallback("OnFrame")

local singleBrandDPS = build.calcsTab.mainOutput.TotalDot
build.configTab.input.BrandsAttachedToEnemy = nil
build.configTab:BuildModList()
runCallback("OnFrame")

assert.are.near(singleBrandDPS * 2, build.calcsTab.mainOutput.TotalDot, 10 ^ -9)
end)

it("averages inverted elemental resistance after penetration", function()
build.skillsTab:PasteSocketGroup("Fireball 20/0 1")
build.configTab.input.enemyIsBoss = "None"
Expand Down
19 changes: 19 additions & 0 deletions spec/System/TestTriggers_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1189,6 +1189,25 @@ describe("TestTriggers", function()
assert.True(build.calcsTab.mainOutput.SkillTriggerRate ~= nil)
end)

it("multiplies Arcanist Brand trigger rate by the attached Brand count", function()
build.skillsTab:PasteSocketGroup("Arcanist Brand 20/0 1\nFireball 20/0 1\n")
runCallback("OnFrame")

local mainSocketGroup = build.skillsTab.socketGroupList[build.mainSocketGroup]
mainSocketGroup.mainActiveSkill = 2
build.modFlag = true
build.buildFlag = true
runCallback("OnFrame")

local singleBrandTriggerRate = build.calcsTab.mainOutput.SkillTriggerRate
build.configTab.input.customMods = "You can have an additional Brand Attached to an Enemy"
build.configTab:BuildModList()
runCallback("OnFrame")

assert.are.near(singleBrandTriggerRate * 2, build.calcsTab.mainOutput.SkillTriggerRate, 10 ^ -9)
assert.matches("2 attached Brands", build.calcsTab.mainEnv.player.mainSkill.infoMessage)
end)

it("Trigger Shockwave", function()
build.itemsTab:CreateDisplayItemFromRaw([[Elemental 1H Mace
Behemoth Mace
Expand Down
30 changes: 20 additions & 10 deletions src/Data/Skills/act_int.lua
Original file line number Diff line number Diff line change
Expand Up @@ -21105,35 +21105,45 @@ skills["WintertideBrand"] = {
statDescriptionScope = "brand_skill_stat_descriptions",
castTime = 0.7,
preDamageFunc = function(activeSkill, output)
activeSkill.skillData.countsAttachedBrandsInDamage = activeSkill.skillPart == 1
activeSkill.skillData.hitTimeOverride = activeSkill.skillData.repeatFrequency / (1 + activeSkill.skillModList:Sum("INC", activeSkill.skillCfg, "Speed", "BrandActivationFrequency") / 100) / activeSkill.skillModList:More(activeSkill.skillCfg, "BrandActivationFrequency")
if activeSkill.skillPart == 2 then
if activeSkill.skillPart == 1 then
local skillMaxStages = activeSkill.skillModList:Sum("BASE", activeSkill.skillCfg, "Multiplier:WintertideBrandMaxStages")
local debuffDurationMult = 1 / math.max(data.misc.BuffExpirationSlowCap, calcLib.mod(activeSkill.actor.enemy.modDB, activeSkill.skillCfg, "BuffExpireFaster"))
local duration = calcSkillDuration(activeSkill.skillModList, activeSkill.skillCfg, activeSkill.skillData, {}) * debuffDurationMult
local maxStages = math.min(duration / activeSkill.skillData.hitTimeOverride + 1, skillMaxStages)
local timeToReachMaxStages = (maxStages - 1) * activeSkill.skillData.hitTimeOverride
local timeAtMaxStages = duration - timeToReachMaxStages
local maxStages = math.min(math.floor(duration / activeSkill.skillData.hitTimeOverride), skillMaxStages)
local timeToReachMaxStages = maxStages * activeSkill.skillData.hitTimeOverride
local timeAtMaxStages = math.max(duration - timeToReachMaxStages, 0)
local damagePerStage = activeSkill.skillModList:Sum("BASE", activeSkill.skillCfg, "Multiplier:WintertideBrandDamagePerStage")
-- Get the average damage before reaching max stages and then damage at max stages
local dpsMultiplier = ((2 + damagePerStage + maxStages * damagePerStage) / 2 * timeToReachMaxStages + timeAtMaxStages * (1 + maxStages * damagePerStage)) / duration
-- Each activation adds one stage; average the completed stages over the attached duration
local averageStages = (maxStages * (maxStages - 1) / 2 * activeSkill.skillData.hitTimeOverride + maxStages * timeAtMaxStages) / duration
local averageDamageMultiplier = averageStages * damagePerStage
local endDurationBase = (activeSkill.skillData.durationTertiary or 0) + activeSkill.skillModList:Sum("BASE", activeSkill.skillCfg, "Duration", "TertiaryDuration")
local endDurationMod = math.max(calcLib.mod(activeSkill.skillModList, activeSkill.skillCfg, "Duration", "TertiaryDuration"), 0)
local endDuration = math.ceil(endDurationBase * endDurationMod * debuffDurationMult * data.misc.ServerTickRate) / data.misc.ServerTickRate
local endUptime = math.min(endDuration / duration, 1)
local attachedBrandCount = activeSkill.skillData.attachedBrandCount
-- Attached Wintertide debuffs stack, but only the strongest maximum-stage Wintertide's End debuff deals damage
local endDamageMultiplier = maxStages * damagePerStage
local dpsMultiplier = attachedBrandCount * (100 + averageDamageMultiplier) + endUptime * (100 + endDamageMultiplier) - 100
activeSkill.skillModList:NewMod("Damage", "MORE", dpsMultiplier, "Wintertide Brand Average Multiplier")
end
end,
parts = {
{
name = "Manual Stages",
stages = true
name = "Average Damage",
},
{
name = "Average Damage",
name = "Manual Stages",
stages = true
}
},
statMap = {
["base_skill_show_average_damage_instead_of_dps"] = {
},
["immolation_brand_burn_damage_+%_final_per_stage"] = {
-- Only apply to Manual Stages part
mod("Damage", "MORE", nil, 0, 0, { type = "Multiplier", var = "WintertideBrandStage", limitVar = "WintertideBrandMaxStages" }, { type = "SkillPart", skillPart = 1 }),
mod("Damage", "MORE", nil, 0, 0, { type = "Multiplier", var = "WintertideBrandStage", limitVar = "WintertideBrandMaxStages" }, { type = "SkillPart", skillPart = 2 }),
mod("Multiplier:WintertideBrandDamagePerStage", "BASE", nil),
},
["winter_brand_max_number_of_stages"] = {
Expand Down
30 changes: 20 additions & 10 deletions src/Export/Skills/act_int.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4594,35 +4594,45 @@ local skills, mod, flag, skill = ...
#skill WintertideBrand
#flags spell area duration brand
preDamageFunc = function(activeSkill, output)
activeSkill.skillData.countsAttachedBrandsInDamage = activeSkill.skillPart == 1
activeSkill.skillData.hitTimeOverride = activeSkill.skillData.repeatFrequency / (1 + activeSkill.skillModList:Sum("INC", activeSkill.skillCfg, "Speed", "BrandActivationFrequency") / 100) / activeSkill.skillModList:More(activeSkill.skillCfg, "BrandActivationFrequency")
if activeSkill.skillPart == 2 then
if activeSkill.skillPart == 1 then
local skillMaxStages = activeSkill.skillModList:Sum("BASE", activeSkill.skillCfg, "Multiplier:WintertideBrandMaxStages")
local debuffDurationMult = 1 / math.max(data.misc.BuffExpirationSlowCap, calcLib.mod(activeSkill.actor.enemy.modDB, activeSkill.skillCfg, "BuffExpireFaster"))
local duration = calcSkillDuration(activeSkill.skillModList, activeSkill.skillCfg, activeSkill.skillData, {}) * debuffDurationMult
local maxStages = math.min(duration / activeSkill.skillData.hitTimeOverride + 1, skillMaxStages)
local timeToReachMaxStages = (maxStages - 1) * activeSkill.skillData.hitTimeOverride
local timeAtMaxStages = duration - timeToReachMaxStages
local maxStages = math.min(math.floor(duration / activeSkill.skillData.hitTimeOverride), skillMaxStages)
local timeToReachMaxStages = maxStages * activeSkill.skillData.hitTimeOverride
local timeAtMaxStages = math.max(duration - timeToReachMaxStages, 0)
local damagePerStage = activeSkill.skillModList:Sum("BASE", activeSkill.skillCfg, "Multiplier:WintertideBrandDamagePerStage")
-- Get the average damage before reaching max stages and then damage at max stages
local dpsMultiplier = ((2 + damagePerStage + maxStages * damagePerStage) / 2 * timeToReachMaxStages + timeAtMaxStages * (1 + maxStages * damagePerStage)) / duration
-- Each activation adds one stage; average the completed stages over the attached duration
local averageStages = (maxStages * (maxStages - 1) / 2 * activeSkill.skillData.hitTimeOverride + maxStages * timeAtMaxStages) / duration
local averageDamageMultiplier = averageStages * damagePerStage
local endDurationBase = (activeSkill.skillData.durationTertiary or 0) + activeSkill.skillModList:Sum("BASE", activeSkill.skillCfg, "Duration", "TertiaryDuration")
local endDurationMod = math.max(calcLib.mod(activeSkill.skillModList, activeSkill.skillCfg, "Duration", "TertiaryDuration"), 0)
local endDuration = math.ceil(endDurationBase * endDurationMod * debuffDurationMult * data.misc.ServerTickRate) / data.misc.ServerTickRate
local endUptime = math.min(endDuration / duration, 1)
local attachedBrandCount = activeSkill.skillData.attachedBrandCount
-- Attached Wintertide debuffs stack, but only the strongest maximum-stage Wintertide's End debuff deals damage
local endDamageMultiplier = maxStages * damagePerStage
local dpsMultiplier = attachedBrandCount * (100 + averageDamageMultiplier) + endUptime * (100 + endDamageMultiplier) - 100
activeSkill.skillModList:NewMod("Damage", "MORE", dpsMultiplier, "Wintertide Brand Average Multiplier")
end
end,
parts = {
{
name = "Manual Stages",
stages = true
name = "Average Damage",
},
{
name = "Average Damage",
name = "Manual Stages",
stages = true
}
},
statMap = {
["base_skill_show_average_damage_instead_of_dps"] = {
},
["immolation_brand_burn_damage_+%_final_per_stage"] = {
-- Only apply to Manual Stages part
mod("Damage", "MORE", nil, 0, 0, { type = "Multiplier", var = "WintertideBrandStage", limitVar = "WintertideBrandMaxStages" }, { type = "SkillPart", skillPart = 1 }),
mod("Damage", "MORE", nil, 0, 0, { type = "Multiplier", var = "WintertideBrandStage", limitVar = "WintertideBrandMaxStages" }, { type = "SkillPart", skillPart = 2 }),
mod("Multiplier:WintertideBrandDamagePerStage", "BASE", nil),
},
["winter_brand_max_number_of_stages"] = {
Expand Down
15 changes: 13 additions & 2 deletions src/Modules/CalcOffence.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1418,6 +1418,7 @@ function calcs.offence(env, actor, activeSkill)
output.BrandAttachmentRange = data.misc.BrandAttachmentRangeBase * calcLib.mod(skillModList, skillCfg, "BrandAttachmentRange")
output.BrandAttachmentRangeMetre = output.BrandAttachmentRange / 10
output.ActiveBrandLimit = skillModList:Sum("BASE", skillCfg, "ActiveBrandLimit")
output.AttachedBrandCount = skillData.attachedBrandCount
if breakdown then
breakdown.BrandAttachmentRange = { radius = output.BrandAttachmentRange }
end
Expand Down Expand Up @@ -1839,6 +1840,11 @@ function calcs.offence(env, actor, activeSkill)

runSkillFunc("preDamageFunc")

if activeSkill.skillTypes[SkillType.Brand] then
local damageLabel = skillData.countsAttachedBrandsInDamage and "Average Damage" or "DPS"
activeSkill.infoMessage = s_format("%s for %d attached Brand%s", damageLabel, output.AttachedBrandCount, output.AttachedBrandCount == 1 and "" or "s")
end

-- Handle corpse and enemy explosions
local monsterLife = skillData.corpseLife or (env.enemyLevel and data.monsterLifeTable[env.enemyLevel] or 100)
if skillData.explodeCorpse and (skillData.corpseLife or env.enemyLevel) then
Expand Down Expand Up @@ -2407,6 +2413,10 @@ function calcs.offence(env, actor, activeSkill)
end
-- Other Misc DPS multipliers (like custom source)
skillData.dpsMultiplier = ( skillData.dpsMultiplier or 1 ) * ( 1 + skillModList:Sum("INC", skillCfg, "DPS") / 100 ) * skillModList:More(skillCfg, "DPS")
if activeSkill.skillTypes[SkillType.Brand] and not skillData.countsAttachedBrandsInDamage then
skillData.dpsMultiplier = skillData.dpsMultiplier * output.AttachedBrandCount
output.SkillDPSMultiplier = (output.SkillDPSMultiplier or 1) * output.AttachedBrandCount
end
if env.configInput.repeatMode == "FINAL" or skillModList:Flag(nil, "OnlyFinalRepeat") then
skillData.dpsMultiplier = skillData.dpsMultiplier / (output.Repeats or 1)
end
Expand Down Expand Up @@ -5683,8 +5693,9 @@ function calcs.offence(env, actor, activeSkill)
if skillModList:Flag(nil, "DotCanStackAsTotems") and skillFlags.totem then
skillFlags.DotCanStack = true
end
output.TotalDot = output.TotalDotInstance
output.TotalDotCalcSection = output.TotalDotInstance
local attachedBrandCount = activeSkill.skillTypes[SkillType.Brand] and not skillData.countsAttachedBrandsInDamage and output.AttachedBrandCount or 1
output.TotalDot = attachedBrandCount > 1 and m_min(output.TotalDotInstance * attachedBrandCount, data.misc.DotDpsCap) or output.TotalDotInstance
output.TotalDotCalcSection = output.TotalDot
end

--Calculates and displays cost per second for skills that don't already have one (link skills)
Expand Down
11 changes: 6 additions & 5 deletions src/Modules/CalcPerform.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1320,14 +1320,15 @@ function calcs.perform(env, skipEHP)

for _, activeSkill in ipairs(env.player.activeSkillList) do
if activeSkill.skillTypes[SkillType.Brand] then
local attachLimit = activeSkill.skillModList:Sum("BASE", activeSkill.skillCfg, "BrandsAttachedLimit")
local attached = modDB:Sum("BASE", nil, "Multiplier:ConfigBrandsAttachedToEnemy")
local attachLimit = m_min(activeSkill.skillModList:Sum("BASE", activeSkill.skillCfg, "BrandsAttachedLimit"), activeSkill.skillModList:Sum("BASE", activeSkill.skillCfg, "ActiveBrandLimit"))
local configured = modDB:Sum("BASE", nil, "Multiplier:ConfigBrandsAttachedToEnemy")
local attached = configured > 0 and m_min(configured, attachLimit) or attachLimit
activeSkill.skillData.attachedBrandCount = attached
local activeBrands = modDB:Sum("BASE", nil, "Multiplier:ConfigActiveBrands")
local actual = m_min(attachLimit, attached)
-- Cap the number of active brands by the limit, which is 3 by default
modDB.multipliers["ActiveBrand"] = m_min(activeBrands, modDB:Sum("BASE", nil, "ActiveBrandLimit"))
modDB.multipliers["BrandsAttachedToEnemy"] = m_max(actual, modDB.multipliers["BrandsAttachedToEnemy"] or 0)
enemyDB.multipliers["BrandsAttached"] = m_max(actual, enemyDB.multipliers["BrandsAttached"] or 0)
modDB.multipliers["BrandsAttachedToEnemy"] = m_max(attached, modDB.multipliers["BrandsAttachedToEnemy"] or 0)
enemyDB.multipliers["BrandsAttached"] = m_max(attached, enemyDB.multipliers["BrandsAttached"] or 0)
end
if activeSkill.skillFlags.totem then
local limit = env.player.mainSkill.skillModList:Sum("BASE", env.player.mainSkill.skillCfg, "ActiveTotemLimit", "ActiveBallistaLimit" )
Expand Down
8 changes: 7 additions & 1 deletion src/Modules/CalcTriggers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ local function defaultTriggerHandler(env, config)
s_format("%.2f ^8(base activation cooldown of %s)", actor.mainSkill.triggeredBy.mainSkill.skillData.repeatFrequency, config.triggerName),
s_format("* %.2f ^8(more activation frequency)", actor.mainSkill.triggeredBy.activationFreqMore),
s_format("* %.2f ^8(increased activation frequency)", actor.mainSkill.triggeredBy.activationFreqInc),
s_format("* %d ^8(attached Brands)", actor.mainSkill.triggeredBy.attachedBrandCount),
s_format("= %.2f ^8(activation rate of %s)", trigRate, actor.mainSkill.triggeredBy.mainSkill.activeEffect.grantedEffect.name)
}
elseif breakdown then
Expand Down Expand Up @@ -878,6 +879,10 @@ local function defaultTriggerHandler(env, config)
else
actor.mainSkill.infoMessage = actor.mainSkill.triggeredBy and actor.mainSkill.triggeredBy.grantedEffect.name or config.triggerName .. " Trigger"
end
if actor.mainSkill.skillData.triggeredByBrand then
local attachedBrandCount = actor.mainSkill.triggeredBy.attachedBrandCount
actor.mainSkill.infoMessage = actor.mainSkill.infoMessage .. ":" .. s_format("%d attached Brand%s", attachedBrandCount, attachedBrandCount == 1 and "" or "s")
end

actor.mainSkill.infoTrigger = config.triggerName
end
Expand Down Expand Up @@ -1322,8 +1327,9 @@ local configTable = {
local activationFreqMore = env.player.mainSkill.triggeredBy.mainSkill.skillModList:More(env.player.mainSkill.triggeredBy.mainSkill.skillCfg, "BrandActivationFrequency")
env.player.mainSkill.triggeredBy.activationFreqInc = activationFreqInc
env.player.mainSkill.triggeredBy.activationFreqMore = activationFreqMore
env.player.mainSkill.triggeredBy.attachedBrandCount = env.player.mainSkill.triggeredBy.mainSkill.skillData.attachedBrandCount
env.player.mainSkill.triggeredBy.ignoresTickRate = true
return {trigRate = env.player.mainSkill.triggeredBy.mainSkill.skillData.repeatFrequency * activationFreqInc * activationFreqMore,
return {trigRate = env.player.mainSkill.triggeredBy.mainSkill.skillData.repeatFrequency * activationFreqInc * activationFreqMore * env.player.mainSkill.triggeredBy.attachedBrandCount,
source = env.player.mainSkill.triggeredBy.mainSkill,
triggeredSkillCond = function(env, skill) return skill.skillData.triggeredByBrand and slotMatch(env, skill) end}
end
Expand Down
Loading