From 4226baa176336842d204c6fa15af8886525598ab Mon Sep 17 00:00:00 2001 From: Edvinas Date: Thu, 4 Sep 2025 14:31:44 +0300 Subject: [PATCH 1/3] Add support for deflection --- src/Modules/BuildDisplayStats.lua | 4 ++- src/Modules/CalcDefence.lua | 47 +++++++++++++++++++++---------- src/Modules/CalcSections.lua | 14 ++++++++- src/Modules/Data.lua | 1 + src/Modules/ModParser.lua | 8 +++++- 5 files changed, 56 insertions(+), 18 deletions(-) diff --git a/src/Modules/BuildDisplayStats.lua b/src/Modules/BuildDisplayStats.lua index b748ab7b0..db471fdd2 100644 --- a/src/Modules/BuildDisplayStats.lua +++ b/src/Modules/BuildDisplayStats.lua @@ -156,13 +156,15 @@ local displayStats = { { stat = "NetManaRegen", label = "Net Mana Recovery", fmt = "+.1f", color = colorCodes.MANA }, { stat = "NetEnergyShieldRegen", label = "Net ES Recovery", fmt = "+.1f", color = colorCodes.ES }, { }, - { stat = "Evasion", label = "Evasion rating", fmt = "d", color = colorCodes.EVASION, compPercent = true }, + { stat = "Evasion", label = "Evasion Rating", fmt = "d", color = colorCodes.EVASION, compPercent = true }, { stat = "Spec:EvasionInc", label = "%Inc Evasion from Tree", color = colorCodes.EVASION, fmt = "d%%" }, { stat = "EvadeChance", label = "Evade Chance", fmt = "d%%", color = colorCodes.EVASION, condFunc = function(v,o) return v > 0 and o.noSplitEvade end }, { stat = "MeleeEvadeChance", label = "Melee Evade Chance", fmt = "d%%", color = colorCodes.EVASION, condFunc = function(v,o) return v > 0 and o.splitEvade end }, { stat = "ProjectileEvadeChance", label = "Projectile Evade Chance", fmt = "d%%", color = colorCodes.EVASION, condFunc = function(v,o) return v > 0 and o.splitEvade end }, { stat = "SpellEvadeChance", label = "Spell Evade Chance", fmt = "d%%", color = colorCodes.EVASION, condFunc = function(v,o) return v > 0 and o.splitEvade end }, { stat = "SpellProjectileEvadeChance", label = "Spell Proj. Evade Chance", fmt = "d%%", color = colorCodes.EVASION, condFunc = function(v,o) return v > 0 and o.splitEvade end }, + { stat = "DeflectionRating", label = "Deflection Rating", fmt = "d", color = colorCodes.EVASION, compPercent = true }, + { stat = "DeflectChance", label = "Deflect Chance", fmt = "d%%", color = colorCodes.EVASION, compPercent = true }, { }, { stat = "Armour", label = "Armour", fmt = "d", compPercent = true }, { stat = "Spec:ArmourInc", label = "%Inc Armour from Tree", fmt = "d%%" }, diff --git a/src/Modules/CalcDefence.lua b/src/Modules/CalcDefence.lua index b3467f342..3561f452a 100644 --- a/src/Modules/CalcDefence.lua +++ b/src/Modules/CalcDefence.lua @@ -36,12 +36,9 @@ function calcs.hitChance(evasion, accuracy, uncapped) return uncapped and m_max(round(rawChance), 5) or m_max(m_min(round(rawChance), 100), 5) end -- Calculate Deflect chance -function calcs.deflectChance(evasion, accuracy, uncapped) - if accuracy < 0 then - return 5 - end - local rawChance = ( accuracy * 0.9 ) / ( accuracy + evasion * 0.2 ) * 100 - return uncapped and m_max(round(rawChance), 0) or m_max(m_min(round(rawChance), 100), 0) +function calcs.deflectChance(deflection, accuracy) + local rawChance = ( accuracy * 0.9 ) / ( accuracy + deflection * 0.2 ) * 100 + return 100 - m_max(m_min(round(rawChance), 100), 0) end -- Calculate damage reduction from armour, float function calcs.armourReductionF(armour, raw) @@ -336,7 +333,7 @@ end -- Based on code from FR and BS found in act_*.txt ---@param activeSkill/output/breakdown references table passed in from calc offence ---@param sourceType string type of incoming damage - it will be converted (taken as) from this type if applicable ----@param baseDmg for which to calculate the damage +---@param baseDmg string for which to calculate the damage ---@return table of taken damage parts, and number, sum of damages function calcs.applyDmgTakenConversion(activeSkill, output, breakdown, sourceType, baseDmg) local damageBreakdown = { } @@ -1361,6 +1358,13 @@ function calcs.defence(env, actor) output.EnergyShieldRecoveryCap = output.EnergyShield or 0 end + local enemyAccuracy = round(calcLib.val(enemyDB, "Accuracy")) + if modDB:Flag(nil, "EnemyAccuracyDistancePenalty") then + local enemyDistance = m_max(m_min(modDB:Sum("BASE", nil, "Multiplier:enemyDistance"), 120), 20) + local accuracyPenalty = 1 - ((enemyDistance - 20) / 100) * (1 - 0.1) + enemyAccuracy = m_floor(enemyAccuracy * accuracyPenalty) + end + if modDB:Flag(nil, "CannotEvade") or enemyDB:Flag(nil, "CannotBeEvaded") then output.EvadeChance = 0 output.MeleeEvadeChance = 0 @@ -1374,12 +1378,6 @@ function calcs.defence(env, actor) output.SpellEvadeChance = 100 output.SpellProjectileEvadeChance = 100 else - local enemyAccuracy = round(calcLib.val(enemyDB, "Accuracy")) - if modDB:Flag(nil, "EnemyAccuracyDistancePenalty") then - local enemyDistance = m_max(m_min(modDB:Sum("BASE", nil, "Multiplier:enemyDistance"), 120), 20) - local accuracyPenalty = 1 - ((enemyDistance - 20) / 100) * (1 - 0.1) - enemyAccuracy = m_floor(enemyAccuracy * accuracyPenalty) - end local evadeChance = modDB:Sum("BASE", nil, "EvadeChance") local hitChance = calcLib.mod(enemyDB, nil, "HitChance") local evadeMax = modDB:Max(nil, "EvadeChanceMax") or data.misc.EvadeChanceCap @@ -1428,6 +1426,21 @@ function calcs.defence(env, actor) } end end + + output.DeflectionRating = (output.Evasion * modDB:Sum("BASE", nil, "EvasionGainAsDeflection") / 100 + output.Armour * modDB:Sum("BASE", nil, "ArmourGainAsDeflection") / 100) * calcLib.mod(modDB, nil, "DeflectionRating") + output.DeflectChance = calcs.deflectChance(output.DeflectionRating, enemyAccuracy) + if modDB:Flag(nil, "DeflectIsLucky") then + local notDeflect = 1 - output.DeflectChance / 100 + output.DeflectChance = (1 - notDeflect * notDeflect) * 100 + end + output.DeflectEffect = m_min(m_max(data.misc.DeflectEffect + modDB:Sum("BASE", nil, "DeflectEffect"), 0), 100) + if breakdown then + breakdown.DeflectChance = { + s_format("Enemy level: %d ^8(%s the Configuration tab)", env.enemyLevel, env.configInput.enemyLevel and "overridden from" or "can be overridden in"), + s_format("Average enemy accuracy: %d", enemyAccuracy), + s_format("Approximate deflect chance: %d%%", output.DeflectChance), + } + end end local spellSuppressionChance = modDB:Sum("BASE", nil, "SpellSuppressionChance") @@ -3094,6 +3107,7 @@ function calcs.buildDefenceEstimations(env, actor) DamageIn.EnergyShieldWhenHit = (DamageIn.EnergyShieldWhenHit or 0) + output.EnergyShieldOnSuppress * ( damageCategoryConfig == "Average" and 0.5 or 1 ) DamageIn.LifeWhenHit = (DamageIn.LifeWhenHit or 0) + output.LifeOnSuppress * ( damageCategoryConfig == "Average" and 0.5 or 1 ) end + local effectiveDeflectMulti = 1 - output.DeflectChance * output.DeflectEffect / 10000 -- extra avoid chance if damageCategoryConfig == "Projectile" or damageCategoryConfig == "SpellProjectile" then ExtraAvoidChance = ExtraAvoidChance + output.AvoidProjectilesChance @@ -3128,7 +3142,7 @@ function calcs.buildDefenceEstimations(env, actor) end averageAvoidChance = averageAvoidChance + AvoidChance end - DamageIn[damageType] = output[damageType.."TakenHit"] * (blockEffect * suppressionEffect * (1 - AvoidChance / 100)) + DamageIn[damageType] = output[damageType.."TakenHit"] * (blockEffect * suppressionEffect * effectiveDeflectMulti * (1 - AvoidChance / 100)) end -- recoup initialisation if output["anyRecoup"] > 0 then @@ -3144,7 +3158,7 @@ function calcs.buildDefenceEstimations(env, actor) output["LifeBelowHalfLossLostOverTime"] = 0 end averageAvoidChance = averageAvoidChance / 5 - output["ConfiguredDamageChance"] = 100 * (blockEffect * suppressionEffect * (1 - averageAvoidChance / 100)) + output["ConfiguredDamageChance"] = 100 * (blockEffect * suppressionEffect * effectiveDeflectMulti * (1 - averageAvoidChance / 100)) output["NumberOfMitigatedDamagingHits"] = (output["ConfiguredDamageChance"] ~= 100 or DamageIn["TrackRecoupable"] or DamageIn["TrackLifeLossOverTime"] or DamageIn.GainWhenHit) and numberOfHitsToDie(DamageIn) or output["NumberOfDamagingHits"] if breakdown then breakdown["ConfiguredDamageChance"] = { @@ -3156,6 +3170,9 @@ function calcs.buildDefenceEstimations(env, actor) if suppressionEffect ~= 1 then t_insert(breakdown["ConfiguredDamageChance"], s_format("x %.3f ^8(suppression effect)", suppressionEffect)) end + if effectiveDeflectMulti ~= 1 then + t_insert(breakdown["ConfiguredDamageChance"], s_format("x %.3f ^8(deflect effect)", effectiveDeflectMulti)) + end if averageAvoidChance > 0 then t_insert(breakdown["ConfiguredDamageChance"], s_format("x %.2f ^8(chance for avoidance to fail)", 1 - averageAvoidChance / 100)) end diff --git a/src/Modules/CalcSections.lua b/src/Modules/CalcSections.lua index 59b630cab..ded3e3abf 100644 --- a/src/Modules/CalcSections.lua +++ b/src/Modules/CalcSections.lua @@ -1693,12 +1693,24 @@ return { { modName = { "SpellDodgeChanceMax", "SpellDodgeChance" }, }, }, }, } }, -{ defaultCollapsed = false, label = "Spell Suppression", data = { +--[[{ defaultCollapsed = false, label = "Spell Suppression", data = { extra = "{0:output:SpellSuppressionChance}%", { label = "Suppression Ch.", { format = "{0:output:SpellSuppressionChance}% (+{0:output:SpellSuppressionChanceOverCap}%)", { modName = "SpellSuppressionChance" }, }, }, { label = "Suppression Effect", { format = "{0:output:SpellSuppressionEffect}%", { modName = "SpellSuppressionEffect" }, }, }, { label = "Life on Suppression", haveOutput = "LifeOnSuppress", { format = "{0:output:LifeOnSuppress}", { modName = "LifeOnSuppress" }, }, }, { label = "ES on Suppression", haveOutput = "EnergyShieldOnSuppress", { format = "{0:output:EnergyShieldOnSuppress}", { modName = "EnergyShieldOnSuppress" }, }, }, +} },]] +{ defaultCollapsed = false, label = "Deflection", data = { + extra = "{0:output:DeflectChance}%", + { label = "Eva. as Deflection", { format = "{0:mod:1}%", { modName = "EvasionGainAsDeflection", modType = "BASE" }, }, }, + { label = "Arm. as Deflection", { format = "{0:mod:1}%", { modName = "ArmourGainAsDeflection", modType = "BASE" }, }, }, + { label = "Deflection Rating", { format = "{0:output:DeflectionRating}", { modName = "DeflectionRating" }, }, }, + { label = "Deflect Effect", { format = "{0:output:DeflectEffect}%", { modName = "DeflectEffect" }, }, }, + { label = "Deflect Chance", { format = "{0:output:DeflectChance}%", + { breakdown = "DeflectChance" }, + { label = "Player modifiers", modName = { "DeflectChance" } }, + { label = "Enemy modifiers", modName = { "Accuracy" }, enemy = true }, + }, }, } }, } }, -- misc resources diff --git a/src/Modules/Data.lua b/src/Modules/Data.lua index f751e26f8..ffc4441ed 100644 --- a/src/Modules/Data.lua +++ b/src/Modules/Data.lua @@ -181,6 +181,7 @@ data.misc = { -- magic numbers BlockChanceCap = 90, SuppressionChanceCap = 100, SuppressionEffect = 50, + DeflectEffect = 40, AvoidChanceCap = 75, AccuracyFalloffStart = 20, AccuracyFalloffEnd = 90, diff --git a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua index 70353de36..19d4dee2e 100644 --- a/src/Modules/ModParser.lua +++ b/src/Modules/ModParser.lua @@ -319,6 +319,8 @@ local modNameList = { ["to dodge attacks and spell damage"] = { "AttackDodgeChance", "SpellDodgeChance" }, ["to dodge attack and spell hits"] = { "AttackDodgeChance", "SpellDodgeChance" }, ["to dodge attack or spell hits"] = { "AttackDodgeChance", "SpellDodgeChance" }, + ["deflection rating"] = { "DeflectionRating" }, + ["amount of damage prevented by deflection"] = { "DeflectEffect" }, ["to suppress spell damage"] = { "SpellSuppressionChance" }, ["amount of suppressed spell damage prevented"] = { "SpellSuppressionEffect" }, ["to amount of suppressed spell damage prevented"] = { "SpellSuppressionEffect" }, @@ -3750,7 +3752,11 @@ local specialModList = { }, ["critical hits poison the enemy"] = { mod("PoisonChance", "OVERRIDE", 100, { type = "Condition", var = "CriticalStrike" })}, ["always poison on hit with this weapon"] = { mod("PoisonChance", "OVERRIDE", 100 , nil, ModFlag.Weapon, 0, { type = "Condition", var = "{Hand}Attack" }, { type = "SkillType", skillType = SkillType.NonWeaponAttack, neg = true })}, - -- Suppression + -- Suppression / deflection + ["gain deflection rating equal to (%d+)%% of evasion rating"] = function(num) return { mod("EvasionGainAsDeflection", "BASE", num) } end, + ["gain deflection rating equal to (%d+)%% of armour"] = function(num) return { mod("ArmourGainAsDeflection", "BASE", num) } end, + ["prevent %+(%d+)%% of damage from deflected hits"] = function(num) return { mod("DeflectEffect", "BASE", num) } end, + ["chance to deflect is lucky"] = { flag("DeflectIsLucky") }, ["y?o?u?r? ?chance to suppress spell damage is lucky"] = { flag("SpellSuppressionChanceIsLucky") }, ["y?o?u?r? ?chance to suppress spell damage is unlucky"] = { flag("SpellSuppressionChanceIsUnlucky") }, ["prevent %+(%d+)%% of suppressed spell damage"] = function(num) return { mod("SpellSuppressionEffect", "BASE", num) } end, From 59c2f956b345d3b7eb9576ab9bbc45c5af716228 Mon Sep 17 00:00:00 2001 From: Edvinas Date: Thu, 4 Sep 2025 18:29:37 +0300 Subject: [PATCH 2/3] No deflect at 0 rating and DR at 100% --- src/Modules/CalcDefence.lua | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Modules/CalcDefence.lua b/src/Modules/CalcDefence.lua index 3561f452a..e266d416a 100644 --- a/src/Modules/CalcDefence.lua +++ b/src/Modules/CalcDefence.lua @@ -37,6 +37,9 @@ function calcs.hitChance(evasion, accuracy, uncapped) end -- Calculate Deflect chance function calcs.deflectChance(deflection, accuracy) + if deflection < 1 then + return 0 + end local rawChance = ( accuracy * 0.9 ) / ( accuracy + deflection * 0.2 ) * 100 return 100 - m_max(m_min(round(rawChance), 100), 0) end @@ -2358,15 +2361,16 @@ function calcs.buildDefenceEstimations(env, actor) takenMult = (output[damageType.."SpellTakenHitMult"] + output[damageType.."AttackTakenHitMult"]) / 2 spellSuppressMult = output.EffectiveSpellSuppressionChance == 100 and (1 - output.SpellSuppressionEffect / 100 / 2) or 1 end + local deflectMulti = output.DeflectChance == 100 and (1 - output.DeflectEffect / 100) or 1 output[damageType.."EffectiveAppliedArmour"] = effectiveAppliedArmour output[damageType.."ResistTakenHitMulti"] = resMult - local afterReductionMulti = takenMult * spellSuppressMult + local afterReductionMulti = takenMult * spellSuppressMult * deflectMulti output[damageType.."AfterReductionTakenHitMulti"] = afterReductionMulti local baseMult = resMult * reductMult output[damageType.."BaseTakenHitMult"] = baseMult * afterReductionMulti local takenMultReflect = output[damageType.."TakenReflect"] local finalReflect = baseMult * takenMultReflect - output[damageType.."TakenHit"] = m_max(damage * baseMult + takenFlat, 0) * takenMult * spellSuppressMult + impaleDamage + output[damageType.."TakenHit"] = m_max(damage * baseMult + takenFlat, 0) * afterReductionMulti + impaleDamage output[damageType.."TakenHitMult"] = (damage > 0) and (output[damageType.."TakenHit"] / damage) or 0 output["totalTakenHit"] = output["totalTakenHit"] + output[damageType.."TakenHit"] if output.AnyTakenReflect then @@ -2426,10 +2430,13 @@ function calcs.buildDefenceEstimations(env, actor) if spellSuppressMult ~= 1 then t_insert(breakdown[damageType.."TakenHitMult"], s_format("x Spell Suppression: %.3f", spellSuppressMult)) end + if deflectMulti ~= 1 then + t_insert(breakdown[damageType.."TakenHitMult"], s_format("x Deflection: %.3f", deflectMulti)) + end if impaleDamage ~= 0 then t_insert(breakdown[damageType.."TakenHitMult"], s_format("+ Impale: %.1f", impaleDamage)) end - if takenMult ~= 1 or takenFlat ~= 0 or spellSuppressMult ~= 1 or impaleDamage ~= 0 then + if takenFlat ~= 0 or afterReductionMulti ~= 1 or impaleDamage ~= 0 then t_insert(breakdown[damageType.."TakenHitMult"], s_format("= %.3f", output[damageType.."TakenHitMult"])) end if output.AnyTakenReflect then @@ -3107,7 +3114,7 @@ function calcs.buildDefenceEstimations(env, actor) DamageIn.EnergyShieldWhenHit = (DamageIn.EnergyShieldWhenHit or 0) + output.EnergyShieldOnSuppress * ( damageCategoryConfig == "Average" and 0.5 or 1 ) DamageIn.LifeWhenHit = (DamageIn.LifeWhenHit or 0) + output.LifeOnSuppress * ( damageCategoryConfig == "Average" and 0.5 or 1 ) end - local effectiveDeflectMulti = 1 - output.DeflectChance * output.DeflectEffect / 10000 + local effectiveDeflectMulti = output.DeflectChance < 100 and 1 - output.DeflectChance * output.DeflectEffect / 10000 or 1 -- extra avoid chance if damageCategoryConfig == "Projectile" or damageCategoryConfig == "SpellProjectile" then ExtraAvoidChance = ExtraAvoidChance + output.AvoidProjectilesChance From 87bd925dbcb46205cd0694e5f50da64573acc195 Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Tue, 9 Sep 2025 19:43:19 +1000 Subject: [PATCH 3/3] Use data from Misc.lua --- src/Modules/Data.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Modules/Data.lua b/src/Modules/Data.lua index ffc4441ed..538968f8f 100644 --- a/src/Modules/Data.lua +++ b/src/Modules/Data.lua @@ -181,7 +181,7 @@ data.misc = { -- magic numbers BlockChanceCap = 90, SuppressionChanceCap = 100, SuppressionEffect = 50, - DeflectEffect = 40, + DeflectEffect = data.gameConstants["BasePercentDamageDeflected"], AvoidChanceCap = 75, AccuracyFalloffStart = 20, AccuracyFalloffEnd = 90,