From d6525dc7a646d29a37caf0abb59885baa7463451 Mon Sep 17 00:00:00 2001 From: cupkax Date: Mon, 10 Aug 2026 12:50:15 +1000 Subject: [PATCH] Fix pboc hit rate incorrectly using activation frequency --- src/Data/Skills/act_int.lua | 6 +++++- src/Export/Skills/act_int.txt | 6 +++++- src/Modules/CalcOffence.lua | 31 +++++++++++++++++++++++++++++-- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/Data/Skills/act_int.lua b/src/Data/Skills/act_int.lua index c925c97471..276fe4fe07 100644 --- a/src/Data/Skills/act_int.lua +++ b/src/Data/Skills/act_int.lua @@ -14013,7 +14013,11 @@ skills["PenanceBrandAltY"] = { statDescriptionScope = "brand_skill_stat_descriptions", castTime = 0.75, preDamageFunc = function(activeSkill, output) - activeSkill.skillData.hitTimeOverride = activeSkill.skillData.repeatFrequency / (1 + activeSkill.skillModList:Sum("INC", activeSkill.skillCfg, "Speed", "BrandActivationFrequency") / 100) / activeSkill.skillModList:More(activeSkill.skillCfg, "BrandActivationFrequency") + -- Activating only spreads energy; damage is dealt once, when the brand detaches at the end + -- of its attached duration, so this brand hits once per cast rather than once per activation + activeSkill.skillData.brandActivationTime = activeSkill.skillData.repeatFrequency / (1 + activeSkill.skillModList:Sum("INC", activeSkill.skillCfg, "Speed", "BrandActivationFrequency") / 100) / activeSkill.skillModList:More(activeSkill.skillCfg, "BrandActivationFrequency") + activeSkill.skillData.hitTimeOverride = output.Duration or 0 + activeSkill.skillData.hitTimeCappedByCastTime = true end, baseFlags = { spell = true, diff --git a/src/Export/Skills/act_int.txt b/src/Export/Skills/act_int.txt index 7a5a50a92d..0d3bf56557 100644 --- a/src/Export/Skills/act_int.txt +++ b/src/Export/Skills/act_int.txt @@ -3322,7 +3322,11 @@ local skills, mod, flag, skill = ... #skill PenanceBrandAltY #flags spell area duration brand preDamageFunc = function(activeSkill, output) - activeSkill.skillData.hitTimeOverride = activeSkill.skillData.repeatFrequency / (1 + activeSkill.skillModList:Sum("INC", activeSkill.skillCfg, "Speed", "BrandActivationFrequency") / 100) / activeSkill.skillModList:More(activeSkill.skillCfg, "BrandActivationFrequency") + -- Activating only spreads energy; damage is dealt once, when the brand detaches at the end + -- of its attached duration, so this brand hits once per cast rather than once per activation + activeSkill.skillData.brandActivationTime = activeSkill.skillData.repeatFrequency / (1 + activeSkill.skillModList:Sum("INC", activeSkill.skillCfg, "Speed", "BrandActivationFrequency") / 100) / activeSkill.skillModList:More(activeSkill.skillCfg, "BrandActivationFrequency") + activeSkill.skillData.hitTimeOverride = output.Duration or 0 + activeSkill.skillData.hitTimeCappedByCastTime = true end, #baseMod skill("radius", 28) #baseMod skill("showAverage", true) diff --git a/src/Modules/CalcOffence.lua b/src/Modules/CalcOffence.lua index 77a0b0288b..0d65349d57 100644 --- a/src/Modules/CalcOffence.lua +++ b/src/Modules/CalcOffence.lua @@ -2107,6 +2107,10 @@ function calcs.offence(env, actor, activeSkill) local storedMainHandAccuracy = nil local storedMainHandAccuracyVsEnemy = nil local storedSustainedTraumaBreakdown = { } + -- Skills flagged hitTimeCappedByCastTime hit once per use, so their total hit rate is limited by + -- how fast those uses can be created. Brands need one use per attached brand, matching the + -- AttachedBrandCount factor applied to dpsMultiplier below + local hitInstancesPerUse = (activeSkill.skillTypes[SkillType.Brand] and not skillData.countsAttachedBrandsInDamage and output.AttachedBrandCount) or 1 -- Calculate how often you hit (speed, accuracy, block, etc) for _, pass in ipairs(passList) do globalOutput, globalBreakdown = output, breakdown @@ -2402,10 +2406,15 @@ function calcs.offence(env, actor, activeSkill) end if skillData.hitTimeOverride and not skillData.triggeredOnDeath then output.HitTime = skillData.hitTimeOverride + if skillData.hitTimeCappedByCastTime and output.Time then + -- Skill hits once per use, so it cannot hit more often than those uses can be created + output.HitTime = m_max(output.HitTime, output.Time * hitInstancesPerUse) + end output.HitSpeed = 1 / output.HitTime --Brands always have hitTimeOverride if activeSkill.skillTypes[SkillType.Brand] and not skillModList:Flag(nil, "UnlimitedBrandDuration") then - output.BrandTicks = m_floor(output.Duration * output.HitSpeed) + -- Brands that don't hit on every activation track their activation interval separately + output.BrandTicks = m_floor(output.Duration / (skillData.brandActivationTime or output.HitTime)) end elseif skillData.hitTimeMultiplier and output.Time and not skillData.triggeredOnDeath then output.HitTime = output.Time * skillData.hitTimeMultiplier @@ -2473,6 +2482,10 @@ function calcs.offence(env, actor, activeSkill) end if skillData.hitTimeOverride and not skillData.triggeredOnDeath then output.HitTime = skillData.hitTimeOverride + if skillData.hitTimeCappedByCastTime and output.Time then + -- Skill hits once per use, so it cannot hit more often than those uses can be created + output.HitTime = m_max(output.HitTime, output.Time * hitInstancesPerUse) + end output.HitSpeed = 1 / output.HitTime elseif skillData.hitTimeMultiplier and output.Time and not skillData.triggeredOnDeath then output.HitTime = output.Time * skillData.hitTimeMultiplier @@ -2488,7 +2501,21 @@ function calcs.offence(env, actor, activeSkill) if breakdown then if skillData.hitTimeOverride and not skillData.triggeredOnDeath then breakdown.HitSpeed = { } - t_insert(breakdown.HitSpeed, s_format("1 / %.2f ^8(hit time override)", output.HitTime)) + if skillData.hitTimeCappedByCastTime and output.Time then + local useType = isAttack and "attack" or "cast" + t_insert(breakdown.HitSpeed, "Skill only hits once per use:") + t_insert(breakdown.HitSpeed, s_format("%.2fs ^8(base hit time)", skillData.hitTimeOverride)) + if hitInstancesPerUse > 1 then + t_insert(breakdown.HitSpeed, s_format("%.2fs ^8(%s time x %d attached brands)", output.Time * hitInstancesPerUse, useType, hitInstancesPerUse)) + else + t_insert(breakdown.HitSpeed, s_format("%.2fs ^8(%s time)", output.Time, useType)) + end + t_insert(breakdown.HitSpeed, s_format("= %.2fs ^8(hit time, higher of the two)", output.HitTime)) + t_insert(breakdown.HitSpeed, s_format("")) + t_insert(breakdown.HitSpeed, s_format("1 / %.2f ^8(hit time)", output.HitTime)) + else + t_insert(breakdown.HitSpeed, s_format("1 / %.2f ^8(hit time override)", output.HitTime)) + end t_insert(breakdown.HitSpeed, s_format("= %.2f", output.HitSpeed)) elseif skillData.hitTimeMultiplier and output.Time and not skillData.triggeredOnDeath then breakdown.HitTime = { }