diff --git a/spec/System/TestAftershock_spec.lua b/spec/System/TestAftershock_spec.lua new file mode 100644 index 0000000000..be02e3671b --- /dev/null +++ b/spec/System/TestAftershock_spec.lua @@ -0,0 +1,87 @@ +describe("TestAftershock", function() + before_each(function() + newBuild() + end) + + teardown(function() + -- newBuild() takes care of resetting everything in setup() + end) + + local function setupSkill(weaponBase, gemLine, customMods) + if weaponBase then + build.itemsTab:CreateDisplayItemFromRaw("New Item\n" .. weaponBase) + build.itemsTab:AddDisplayItem() + end + build.skillsTab:PasteSocketGroup(gemLine) + build.mainSocketGroup = 1 + build.configTab.input.customMods = customMods or "" + build.configTab:BuildModList() + runCallback("OnFrame") + return build.calcsTab.mainOutput + end + + local function assertClose(expected, actual) + assert.is_true(math.abs(expected - actual) < 0.001, string.format("expected %s, got %s", tostring(expected), tostring(actual))) + end + + it("Earthbreaker gives slam skills a chance to cause an additional aftershock, scaling DPS but not average hit", function() + local base = setupSkill("Wooden Club", "Leap Slam 20/0 1") + local baseDPS, baseAvg = base.TotalDPS, base.AverageDamage + assert.is_true(baseDPS > 0) + assert.are.equals(0, base.AftershockChance) + + newBuild() + local out = setupSkill("Wooden Club", "Leap Slam 20/0 1", "25% chance for Slam Skills you use yourself to cause an additional Aftershock") + assert.are.equals(25, out.AftershockChance) + assertClose(baseDPS * 1.25, out.TotalDPS) + assertClose(baseAvg, out.AverageDamage) + end) + + it("does not give non-slam skills an aftershock chance", function() + local out = setupSkill("Wooden Club", "Perfect Strike 20/0 1", "25% chance for Slam Skills you use yourself to cause an additional Aftershock") + assert.is_true(out.TotalDPS > 0) + assert.is_nil(out.AftershockChance) + end) + + it("stacks aftershock chance from passives and support gems without a cap", function() + local base = setupSkill("Wooden Club", "Leap Slam 20/0 1") + local baseDPS = base.TotalDPS + + newBuild() + local out = setupSkill("Wooden Club", "Leap Slam 20/0 1", [[ + 25% chance for Slam Skills you use yourself to cause an additional Aftershock + 10% chance for Mace Slam Skills you use yourself to cause an additional Aftershock + 5% chance for Slam Skills to cause an additional Aftershock + ]]) + assert.are.equals(40, out.AftershockChance) + + newBuild() + out = setupSkill("Wooden Club", "Leap Slam 20/0 1\nAftershock II 20/0 1", "80% chance for Slam Skills you use yourself to cause an additional Aftershock") + assert.are.equals(115, out.AftershockChance) + assertClose(baseDPS * 2.15, out.TotalDPS) + end) + + it("only applies mace slam aftershock chance when attacking with a mace", function() + local out = setupSkill("Hardwood Spear", "Thunderous Leap 20/0 1", "10% chance for Mace Slam Skills you use yourself to cause an additional Aftershock") + assert.is_true(out.TotalDPS > 0) + assert.are.equals(0, out.AftershockChance) + + newBuild() + out = setupSkill("Hardwood Spear", "Thunderous Leap 20/0 1", "25% chance for Slam Skills you use yourself to cause an additional Aftershock") + assert.are.equals(25, out.AftershockChance) + + newBuild() + out = setupSkill("Wooden Club", "Leap Slam 20/0 1", "10% chance for Mace Slam Skills you use yourself to cause an additional Aftershock") + assert.are.equals(10, out.AftershockChance) + end) + + it("only applies shapeshift slam aftershock chance to shapeshift slams", function() + local out = setupSkill("Changeling Talisman", "Rampage 20/0 1", "15% chance for Shapeshift Slam Skills you use yourself to cause an additional Aftershock") + assert.is_true(out.TotalDPS > 0) + assert.are.equals(15, out.AftershockChance) + + newBuild() + out = setupSkill("Wooden Club", "Leap Slam 20/0 1", "15% chance for Shapeshift Slam Skills you use yourself to cause an additional Aftershock") + assert.are.equals(0, out.AftershockChance) + end) +end) diff --git a/spec/System/TestEarthquake_spec.lua b/spec/System/TestEarthquake_spec.lua new file mode 100644 index 0000000000..690b93fe5f --- /dev/null +++ b/spec/System/TestEarthquake_spec.lua @@ -0,0 +1,141 @@ +describe("TestEarthquake", function() + before_each(function() + newBuild() + end) + + teardown(function() + -- newBuild() takes care of resetting everything in setup() + end) + + -- statSetIndex 1 = Impact, 2 = Aftershock + local function setupEarthquake(statSetIndex, customMods, weapons) + for _, weapon in ipairs(weapons or { "Wooden Club" }) do + build.itemsTab:CreateDisplayItemFromRaw("New Item\n" .. weapon) + build.itemsTab:AddDisplayItem() + end + build.skillsTab:PasteSocketGroup("Earthquake 20/0 1") + local gemInstance = build.skillsTab.socketGroupList[1].gemList[1] + gemInstance.statSet = { EarthquakePlayer = statSetIndex } + gemInstance.statSetCalcs = { EarthquakePlayer = statSetIndex } + build.mainSocketGroup = 1 + build.configTab.input.customMods = customMods or "" + build.configTab:BuildModList() + runCallback("OnFrame") + return build.calcsTab.mainOutput + end + + local function assertClose(expected, actual) + assert.is_true(math.abs(expected - actual) < 0.001, string.format("expected %s, got %s", tostring(expected), tostring(actual))) + end + + -- Jagged Ground cannot be created on top of an existing patch: an attack that lands before the previous patch + -- erupts creates nothing, so the next patch (and its Aftershock) comes from the first attack at or after the eruption + local function expectedHitTime(out) + local attackTime = 1 / out.Speed + return attackTime * math.max(math.ceil(out.Duration / attackTime - 1e-9), 1) + end + + it("fires the Aftershock on every Nth attack, N being the attack times needed to cover the patch duration", function() + local out = setupEarthquake(2) + assert.is_true(out.Duration > 0) + assert.is_true(out.Speed > 1 / out.Duration, "test assumes attack time is shorter than the patch duration") + assertClose(expectedHitTime(out), out.HitTime) + assertClose(1 / out.HitTime, out.HitSpeed) + assert.is_true(out.HitTime >= out.Duration - 0.001, "no patch can be created before the previous one erupts") + assert.is_true(out.HitTime - 1 / out.Speed < out.Duration, "the first attack after the eruption creates the next patch") + assertClose(out.AverageDamage * out.HitSpeed * out.DpsMultiplier, out.TotalDPS) + assert.is_falsy(build.calcsTab.mainEnv.player.mainSkill.skillData.showAverage) + end) + + it("rounds the Aftershock time up to a whole number of attacks for any patch duration", function() + for _, reduced in ipairs({ 30, 50, 70, 80, 90 }) do + newBuild() + local out = setupEarthquake(2, reduced .. "% reduced Skill Effect Duration") + assertClose(expectedHitTime(out), out.HitTime) + assertClose(out.AverageDamage / out.HitTime * out.DpsMultiplier, out.TotalDPS) + end + end) + + it("wastes every other attack when the patch outlasts the attack time", function() + local base = setupEarthquake(2) + local attackTime = 1 / base.Speed + -- shorten the patch to 1.5 attack times: the second attack lands on the live patch and creates nothing + local reduced = math.floor(100 * (1 - 1.5 * attackTime / base.Duration)) + newBuild() + local out = setupEarthquake(2, reduced .. "% reduced Skill Effect Duration") + assert.is_true(out.Duration > attackTime and out.Duration < 2 * attackTime, "test assumes the patch lasts between one and two attack times") + assertClose(2 * attackTime, out.HitTime) + assertClose(base.Speed / 2, out.HitSpeed) + end) + + it("lowers Aftershock DPS when attacking slightly faster than the patch duration", function() + local base = setupEarthquake(2) + local attackTime = 1 / base.Speed + -- patch just shorter than the attack time: every attack creates a patch + local reduced = math.ceil(100 * (1 - 0.95 * attackTime / base.Duration)) + newBuild() + local slow = setupEarthquake(2, reduced .. "% reduced Skill Effect Duration") + assert.is_true(slow.Duration <= 1 / slow.Speed, "test assumes the patch erupts before the next attack") + assertClose(slow.Speed, slow.HitSpeed) + -- 10% more attack speed makes the next attack land on the live patch, halving the Aftershock rate + newBuild() + local fast = setupEarthquake(2, reduced .. "% reduced Skill Effect Duration\n10% increased Attack Speed") + assert.is_true(fast.Speed > slow.Speed) + assert.is_true(fast.Duration > 1 / fast.Speed, "test assumes the next attack now lands before the eruption") + assertClose(fast.Speed / 2, fast.HitSpeed) + assert.is_true(fast.TotalDPS < slow.TotalDPS) + end) + + it("uses the attack rate for Aftershock once the patch duration is shorter than the attack time", function() + local out = setupEarthquake(2, "80% reduced Skill Effect Duration") + assert.is_true(out.Speed < 1 / out.Duration, "test assumes attack time is longer than the patch duration") + assertClose(out.Speed, out.HitSpeed) + assertClose(out.AverageDamage * out.Speed * out.DpsMultiplier, out.TotalDPS) + + newBuild() + out = setupEarthquake(2, "100% reduced Skill Effect Duration\n200% increased Attack Speed") + assert.are.equals(0, out.Duration) + assertClose(out.AverageDamage * out.Speed * out.DpsMultiplier, out.TotalDPS) + end) + + it("rounds the Aftershock time using the combined attack time when dual wielding", function() + local weapons = { "Marauding Mace\n20% increased Attack Speed", "Marauding Mace" } + local out = setupEarthquake(2, nil, weapons) + assert.is_true(out.MainHand.Speed ~= out.OffHand.Speed, "test assumes the two weapons attack at different speeds") + assertClose(expectedHitTime(out), out.HitTime) + assertClose(out.AverageDamage * out.HitSpeed * out.DpsMultiplier, out.TotalDPS) + + newBuild() + out = setupEarthquake(2, "85% reduced Skill Effect Duration", weapons) + assert.is_true(out.Speed < 1 / out.Duration, "test assumes the combined attack time is longer than the patch duration") + assert.is_true(out.Speed ~= out.MainHand.Speed, "test assumes the combined attack rate differs from the main hand rate") + assertClose(out.Speed, out.HitSpeed) + assertClose(out.AverageDamage * out.Speed * out.DpsMultiplier, out.TotalDPS) + end) + + it("applies additional aftershock chance to the Aftershock but not to the Impact", function() + local aftershockMod = "25% chance for Slam Skills you use yourself to cause an additional Aftershock" + local base = setupEarthquake(1) + newBuild() + local out = setupEarthquake(1, aftershockMod) + assert.is_falsy(out.AftershockChance and out.AftershockChance > 0) + assertClose(base.TotalDPS, out.TotalDPS) + + newBuild() + base = setupEarthquake(2) + newBuild() + out = setupEarthquake(2, aftershockMod) + assert.are.equals(25, out.AftershockChance) + assertClose(base.TotalDPS * 1.25, out.TotalDPS) + end) + + it("does not cap the Impact hit rate", function() + local base = setupEarthquake(1) + assert.is_nil(base.HitSpeed) + + newBuild() + local out = setupEarthquake(1, "200% increased Attack Speed") + assert.is_nil(out.HitSpeed) + assertClose(base.TotalDPS * out.Speed / base.Speed, out.TotalDPS) + end) +end) diff --git a/src/Data/ModCache.lua b/src/Data/ModCache.lua index 701abf6fd2..267d97b7d3 100644 --- a/src/Data/ModCache.lua +++ b/src/Data/ModCache.lua @@ -1456,9 +1456,8 @@ c["10% chance for Attack Hits to apply ten Incision"]={{[1]={flags=0,keywordFlag c["10% chance for Enemies you Kill to Explode, dealing 100%"]={{}," for Enemies you Kill to Explode, dealing 100% "} c["10% chance for Enemies you Kill to Explode, dealing 100% of their maximum Life as Physical Damage"]={{[1]={flags=0,keywordFlags=0,name="ExplodeMod",type="LIST",value={amount=100,keyOfScaledMod="value",type="Physical",value=10}},[2]={flags=0,keywordFlags=0,name="CanExplode",type="FLAG",value=true}},nil} c["10% chance for Flasks you use to not consume Charges"]={{[1]={flags=0,keywordFlags=0,name="FlaskChanceNotConsumeCharges",type="BASE",value=10}},nil} -c["10% chance for Mace Slam Skills you use yourself to cause an additional Aftershock"]={{}," for Mace Slam Skills you use yourself to cause an additional Aftershock "} -c["10% chance for Mace Slam Skills you use yourself to cause an additional Aftershock Strike Skills you use yourself with Maces have 10% chance to deal Splash Damage"]={{[1]={flags=1048580,keywordFlags=0,name="Damage",type="BASE",value=10}}," for Mace Slam Skills you use yourself to cause an additional Aftershock Strike Skills you use yourself have 10% chance to deal Splash "} -c["10% chance for Shapeshift Slam Skills you use yourself to cause an additional Aftershock"]={{}," for Shapeshift Slam Skills you use yourself to cause an additional Aftershock "} +c["10% chance for Mace Slam Skills you use yourself to cause an additional Aftershock"]={{[1]={[1]={skillType=93,type="SkillType"},flags=1048576,keywordFlags=0,name="AftershockChance",type="BASE",value=10}},nil} +c["10% chance for Shapeshift Slam Skills you use yourself to cause an additional Aftershock"]={{[1]={[1]={skillType=93,type="SkillType"},[2]={skillType=157,type="SkillType"},flags=0,keywordFlags=0,name="AftershockChance",type="BASE",value=10}},nil} c["10% chance to Aggravate Bleeding on targets you Hit with Attacks"]={{}," to Aggravate Bleeding on targets you Hit "} c["10% chance to Aggravate Bleeding on targets you Hit with Attacks 8% increased Attack Speed while a Rare or Unique Enemy is in your Presence"]={{[1]={[1]={actor="enemy",type="ActorCondition",varList={[1]="NearbyRareOrUniqueEnemy",[2]="RareOrUnique"}},flags=1,keywordFlags=65536,name="Speed",type="BASE",value=10}}," to Aggravate Bleeding on targets you Hit 8% increased "} c["10% chance to Avoid Elemental Ailments"]={{[1]={flags=0,keywordFlags=0,name="AvoidElementalAilments",type="BASE",value=10}},nil} @@ -2074,7 +2073,7 @@ c["125% increased amount of Mana Leeched if you've dealt a Critical Hit Recently c["13 Mana gained when you Block"]={{[1]={flags=0,keywordFlags=0,name="ManaOnBlock",type="BASE",value=13}},nil} c["13 to 23 Physical Thorns damage"]={{[1]={flags=32,keywordFlags=0,name="PhysicalMin",type="BASE",value=13},[2]={flags=32,keywordFlags=0,name="PhysicalMax",type="BASE",value=23}},nil} c["13% chance for Flasks you use to not consume Charges"]={{[1]={flags=0,keywordFlags=0,name="FlaskChanceNotConsumeCharges",type="BASE",value=13}},nil} -c["13% chance for Mace Slam Skills you use yourself to cause an additional Aftershock"]={{}," for Mace Slam Skills you use yourself to cause an additional Aftershock "} +c["13% chance for Mace Slam Skills you use yourself to cause an additional Aftershock"]={{[1]={[1]={skillType=93,type="SkillType"},flags=1048576,keywordFlags=0,name="AftershockChance",type="BASE",value=13}},nil} c["13% chance for Spell Skills to fire 2 additional Projectiles"]={{[1]={flags=2,keywordFlags=0,name="TwoAdditionalProjectilesChance",type="BASE",value=13}},nil} c["13% chance to Gain Arcane Surge when you deal a Critical Hit"]={{[1]={[1]={type="Condition",var="CritRecently"},flags=0,keywordFlags=0,name="Condition:ArcaneSurge",type="FLAG",value=true}},nil} c["13% chance to Maim on Hit"]={{}," to Maim "} @@ -2194,7 +2193,7 @@ c["15 to 25 Physical Thorns damage"]={{[1]={flags=32,keywordFlags=0,name="Physic c["15% Surpassing Chance to gain a Puppet Master stack whenever you use a Command Skill"]={{},"% Surpassing Chance to gain a Puppet Master stack whenever you use a Command Skill "} c["15% additional Physical Damage Reduction"]={{[1]={flags=0,keywordFlags=0,name="PhysicalDamageReduction",type="BASE",value=15}},nil} c["15% chance for Remnants you create to grant their effects twice"]={{[1]={flags=0,keywordFlags=0,name="LocalEffect",type="BASE",value=15}}," for Remnants you create to grant their s twice "} -c["15% chance for Shapeshift Slam Skills you use yourself to cause an additional Aftershock"]={{}," for Shapeshift Slam Skills you use yourself to cause an additional Aftershock "} +c["15% chance for Shapeshift Slam Skills you use yourself to cause an additional Aftershock"]={{[1]={[1]={skillType=93,type="SkillType"},[2]={skillType=157,type="SkillType"},flags=0,keywordFlags=0,name="AftershockChance",type="BASE",value=15}},nil} c["15% chance that if you would gain Endurance Charges, you instead gain up to maximum Endurance Charges"]={{[1]={flags=0,keywordFlags=0,name="FlaskCharges",type="BASE",value=15}}," that if you would gain Endurance , you instead gain up to maximum Endurance Charges "} c["15% chance that if you would gain Frenzy Charges, you instead gain up to your maximum number of Frenzy Charges"]={{[1]={flags=0,keywordFlags=0,name="FlaskCharges",type="BASE",value=15}}," that if you would gain Frenzy , you instead gain up to your maximum number of Frenzy Charges "} c["15% chance that if you would gain Power Charges, you instead gain up to your maximum number of Power Charges"]={{[1]={flags=0,keywordFlags=0,name="FlaskCharges",type="BASE",value=15}}," that if you would gain Power , you instead gain up to your maximum number of Power Charges "} @@ -2745,7 +2744,7 @@ c["20% chance for Energy Shield Recharge to start when you Kill an Enemy"]={{[1] c["20% chance for Flasks you use to not consume Charges"]={{[1]={flags=0,keywordFlags=0,name="FlaskChanceNotConsumeCharges",type="BASE",value=20}},nil} c["20% chance for Lightning Damage with Hits to be Lucky"]={{[1]={flags=0,keywordFlags=0,name="LightningLuckyHitsChance",type="BASE",value=20}},nil} c["20% chance for Lightning Skills to Chain an additional time"]={{[1]={flags=0,keywordFlags=128,name="ChainChance",type="BASE",value=20}},nil} -c["20% chance for Mace Slam Skills you use yourself to cause an additional Aftershock"]={{}," for Mace Slam Skills you use yourself to cause an additional Aftershock "} +c["20% chance for Mace Slam Skills you use yourself to cause an additional Aftershock"]={{[1]={[1]={skillType=93,type="SkillType"},flags=1048576,keywordFlags=0,name="AftershockChance",type="BASE",value=20}},nil} c["20% chance to Aggravate Bleeding on targets you Critically Hit with Attacks"]={{}," to Aggravate Bleeding on targets you Critically Hit "} c["20% chance to Aggravate Bleeding on targets you Hit with Empowered Attacks"]={{}," to Aggravate Bleeding on targets you Hit with Empowered Attacks "} c["20% chance to Aggravate Bleeding on targets you Hit with Empowered Attacks Empowered Attacks deal 30% increased Damage"]={{[1]={flags=0,keywordFlags=0,name="Damage",type="BASE",value=20}}," to Aggravate Bleeding on targets you Hit with Empowered Attacks Empowered Attacks deal 30% increased "} @@ -3181,7 +3180,7 @@ c["25% chance for Attacks to Maim on Hit against Poisoned Enemies 25% increased c["25% chance for Lightning Damage with Hits to be Lucky"]={{[1]={flags=0,keywordFlags=0,name="LightningLuckyHitsChance",type="BASE",value=25}},nil} c["25% chance for Projectiles to Pierce Enemies within 3m distance of you"]={{[1]={flags=0,keywordFlags=0,name="ProjectileCount",type="BASE",value=25}}," for to Pierce Enemies within 3m distance of you "} c["25% chance for Skills to retain 40% of Glory on use"]={{}," for Skills to retain 40% of Glory on use "} -c["25% chance for Slam Skills you use yourself to cause an additional Aftershock"]={{}," for Slam Skills you use yourself to cause an additional Aftershock "} +c["25% chance for Slam Skills you use yourself to cause an additional Aftershock"]={{[1]={[1]={skillType=93,type="SkillType"},flags=0,keywordFlags=0,name="AftershockChance",type="BASE",value=25}},nil} c["25% chance for Trigger skills to refund half of Energy Spent"]={{}," for Trigger skills to refund half of Energy Spent "} c["25% chance on Consuming a Shock on an Enemy to reapply it"]={{}," on Consuming a Shock on an Enemy to reapply it "} c["25% chance on Shocking Enemies to created Shocked Ground"]={{[1]={[1]={actor="enemy",type="ActorCondition",var="OnShockedGround"},flags=0,keywordFlags=0,name="ShockBase",type="BASE",value=20}},nil} @@ -4336,8 +4335,8 @@ c["5 to 10 Physical Thorns damage"]={{[1]={flags=32,keywordFlags=0,name="Physica c["5 to 9 Physical Thorns damage"]={{[1]={flags=32,keywordFlags=0,name="PhysicalMin",type="BASE",value=5},[2]={flags=32,keywordFlags=0,name="PhysicalMax",type="BASE",value=9}},nil} c["5% Chance to build an additional Combo on Hit"]={{}," to build an additional Combo "} c["5% additional Physical Damage Reduction while you have at least 150 Devotion"]={{[1]={[1]={stat="Devotion",threshold=150,type="StatThreshold"},flags=0,keywordFlags=0,name="PhysicalDamageReduction",type="BASE",value=5}},nil} -c["5% chance for Slam Skills to cause an additional Aftershock"]={{}," for Slam Skills to cause an additional Aftershock "} -c["5% chance for Slam Skills you use yourself to cause an additional Aftershock"]={{}," for Slam Skills you use yourself to cause an additional Aftershock "} +c["5% chance for Slam Skills to cause an additional Aftershock"]={{[1]={[1]={skillType=93,type="SkillType"},flags=0,keywordFlags=0,name="AftershockChance",type="BASE",value=5}},nil} +c["5% chance for Slam Skills you use yourself to cause an additional Aftershock"]={{[1]={[1]={skillType=93,type="SkillType"},flags=0,keywordFlags=0,name="AftershockChance",type="BASE",value=5}},nil} c["5% chance to Blind Enemies on Hit"]={{[1]={flags=0,keywordFlags=0,name="BlindChance",type="BASE",value=5}},nil} c["5% chance to Blind Enemies on Hit with Attacks"]={{[1]={flags=0,keywordFlags=65536,name="BlindChance",type="BASE",value=5}},nil} c["5% chance to Daze on Hit"]={{[1]={flags=4,keywordFlags=0,name="DazeChance",type="BASE",value=5}},nil} @@ -5001,8 +5000,8 @@ c["75% increased chance to Shock"]={{[1]={flags=0,keywordFlags=0,name="EnemyShoc c["75% increased effect of Socketed Augment Items"]={{[1]={flags=0,keywordFlags=0,name="SocketedAugmentItemEffect",type="INC",value=75}},nil} c["75% more Stun Buildup with Lightning Damage"]={{[1]={[1]={type="Condition",var="LightningHasDamage"},flags=0,keywordFlags=0,name="EnemyHeavyStunBuildup",type="MORE",value=75}},nil} c["75% of Damage Converted to Fire Damage"]={{[1]={flags=0,keywordFlags=0,name="DamageConvertToFire",type="BASE",value=75}},nil} -end)();(function() c["75% of Volatility Physical Damage Taken as Cold Damage"]={{[1]={flags=0,keywordFlags=0,name="PhysicalDamageTakenAsCold",type="BASE",value=75}}," Volatility "} +end)();(function() c["75% reduced Amount Recovered"]={{[1]={flags=0,keywordFlags=0,name="FlaskRecovery",type="INC",value=-75}},nil} c["75% reduced Charges per use"]={{[1]={flags=0,keywordFlags=0,name="FlaskChargesUsed",type="INC",value=-75}},nil} c["75% reduced Ignite Duration on Enemies"]={{[1]={flags=0,keywordFlags=0,name="EnemyIgniteDuration",type="INC",value=-75}},nil} @@ -5013,8 +5012,8 @@ c["8 Life Regeneration per second"]={{[1]={flags=0,keywordFlags=0,name="LifeRege c["8 to 14 Fire Damage per Endurance Charge"]={{[1]={[1]={type="Multiplier",var="EnduranceCharge"},flags=0,keywordFlags=0,name="FireMin",type="BASE",value=8},[2]={[1]={type="Multiplier",var="EnduranceCharge"},flags=0,keywordFlags=0,name="FireMax",type="BASE",value=14}},nil} c["8% Global chance to Blind Enemies on Hit"]={{[1]={[1]={type="Global"},flags=0,keywordFlags=0,name="BlindChance",type="BASE",value=8}},"% chance "} c["8% additional Physical Damage Reduction"]={{[1]={flags=0,keywordFlags=0,name="PhysicalDamageReduction",type="BASE",value=8}},nil} -c["8% chance for Mace Slam Skills you use yourself to cause an additional Aftershock"]={{}," for Mace Slam Skills you use yourself to cause an additional Aftershock "} -c["8% chance for Slam Skills you use yourself to cause an additional Aftershock"]={{}," for Slam Skills you use yourself to cause an additional Aftershock "} +c["8% chance for Mace Slam Skills you use yourself to cause an additional Aftershock"]={{[1]={[1]={skillType=93,type="SkillType"},flags=1048576,keywordFlags=0,name="AftershockChance",type="BASE",value=8}},nil} +c["8% chance for Slam Skills you use yourself to cause an additional Aftershock"]={{[1]={[1]={skillType=93,type="SkillType"},flags=0,keywordFlags=0,name="AftershockChance",type="BASE",value=8}},nil} c["8% chance for Spell Skills to fire 2 additional Projectiles"]={{[1]={flags=2,keywordFlags=0,name="TwoAdditionalProjectilesChance",type="BASE",value=8}},nil} c["8% chance for Spell Skills to fire 8 additional Projectiles in a circle"]={{[1]={flags=2,keywordFlags=0,name="ProjectileCount",type="BASE",value=8}}," to fire 8 additional in a circle "} c["8% chance to Aggravate Bleeding on targets you Hit with Attacks"]={{}," to Aggravate Bleeding on targets you Hit "} @@ -10003,8 +10002,8 @@ c["for 4 seconds, every 0.25 seconds while raised"]={nil,"for 4 seconds, every 0 c["gain 6 Cold Surges or 6 Fire Surges"]={{}," Cold Surges or 6 Fire Surges "} c["the enemy's Power for 20 seconds, up to a total of 500"]={nil,"the enemy's Power for 20 seconds, up to a total of 500 "} c["their Explicit Modifiers are transformed into more powerful related Modifiers"]={nil,"their Explicit Modifiers are transformed into more powerful related Modifiers "} -end)();(function() c["their Explicit Modifiers are transformed into more powerful related Modifiers Ignore Attribute Requirements to equip Gloves"]={nil,"their Explicit Modifiers are transformed into more powerful related Modifiers Ignore Attribute Requirements to equip Gloves "} +end)();(function() c["until you take no Damage to Life for 3 seconds"]={nil,"until you take no Damage to Life for 3 seconds "} c["until you take no Damage to Life for 5 seconds"]={nil,"until you take no Damage to Life for 5 seconds "} c["until you take no Damage to Life for 5 seconds Life that would be lost by taking Damage is instead Reserved"]={nil,"until you take no Damage to Life for 5 seconds Life that would be lost by taking Damage is instead Reserved "} diff --git a/src/Data/Skills/act_str.lua b/src/Data/Skills/act_str.lua index 6cf0aaaa48..bb1f3c1e8a 100644 --- a/src/Data/Skills/act_str.lua +++ b/src/Data/Skills/act_str.lua @@ -4142,6 +4142,9 @@ skills["EarthquakePlayer"] = { area = true, duration = true, }, + baseMods = { + skill("cannotCauseAftershock", true), + }, constantStats = { { "skill_jagged_ground_base_duration_ms", 4000 }, { "jagged_ground_effect_+%", 100 }, @@ -4217,9 +4220,12 @@ skills["EarthquakePlayer"] = { attack = true, melee = true, area = true, + duration = true, }, baseMods = { skill("showAverage", true), + skill("duration", 4), + skill("hitRateLimitedByDuration", true), }, constantStats = { { "jagged_ground_effect_+%", 100 }, diff --git a/src/Export/Skills/act_str.txt b/src/Export/Skills/act_str.txt index 2cda812713..649627e55e 100644 --- a/src/Export/Skills/act_str.txt +++ b/src/Export/Skills/act_str.txt @@ -303,9 +303,10 @@ statMap = { mod("AilmentMagnitude", "MORE", nil, 0, bit.bor(KeywordFlag.Poison, KeywordFlag.Bleed, KeywordFlag.Ignite)), }, }, +#baseMod skill("cannotCauseAftershock", true) #mods #set EarthquakeAftershockPlayer -#flags attack melee area +#flags attack melee area duration statMap = { ["jagged_ground_effect_+%"] = { mod("Damage", "MORE", nil, 0, 0), @@ -315,6 +316,8 @@ statMap = { }, }, #baseMod skill("showAverage", true) +#baseMod skill("duration", 4) +#baseMod skill("hitRateLimitedByDuration", true) #mods #skillEnd diff --git a/src/Modules/CalcOffence.lua b/src/Modules/CalcOffence.lua index 960a5e16d4..4f82492117 100644 --- a/src/Modules/CalcOffence.lua +++ b/src/Modules/CalcOffence.lua @@ -432,6 +432,14 @@ function calcSkillCooldown(skillModList, skillCfg, skillData) end end +-- Time between hits of a skill that can only hit once per duration-long effect that cannot be refreshed early +-- (e.g. Earthquake's Aftershock: Jagged Ground cannot be created on top of an existing patch). A use that lands +-- before the previous effect ends creates nothing, so the next effect comes from the first use at or after the end +local function calcDurationLimitedHitTime(useTime, duration) + local usesPerHit = m_max(m_ceil(duration / useTime - 1e-9), 1) + return useTime * usesPerHit, usesPerHit +end + local function calcWarcryCastTime(skillModList, skillCfg, skillData, actor) local baseSpeed = 1 / skillModList:Sum("BASE", skillCfg, "WarcryCastTime") local warcryCastTime = baseSpeed * calcLib.mod(skillModList, skillCfg, "WarcrySpeed") * calcs.actionSpeedMod(actor) @@ -3008,7 +3016,7 @@ function calcs.offence(env, actor, activeSkill) output.Speed = m_min(output.Speed, 1 / output.Cooldown * output.Repeats) end end - if output.Cooldown and skillFlags.selfCast or skillData.maxHitRatePerEnemy or skillData.hitTimeOverride then + if output.Cooldown and skillFlags.selfCast or skillData.maxHitRatePerEnemy or skillData.hitTimeOverride or skillData.hitRateLimitedByDuration then skillFlags.notAverage = true skillFlags.showAverage = false skillData.showAverage = false @@ -3143,6 +3151,10 @@ function calcs.offence(env, actor, activeSkill) if skillFlags.brand and not skillModList:Flag(nil, "UnlimitedBrandDuration") then output.BrandTicks = m_floor(output.Duration * output.HitSpeed) end + elseif skillData.hitRateLimitedByDuration and output.Time and globalOutput.Duration and globalOutput.Duration > 0 and not skillData.triggeredOnDeath then + -- Only every Nth use creates the effect that hits (e.g. Earthquake's Jagged Ground), see calcDurationLimitedHitTime + output.HitTime, output.UsesPerHit = calcDurationLimitedHitTime(output.Time, globalOutput.Duration) + output.HitSpeed = 1 / output.HitTime elseif skillData.hitTimeMultiplier and output.Time and not skillData.triggeredOnDeath then output.HitTime = output.Time * skillData.hitTimeMultiplier if skillFlags.channelRelease and skillData.minChannelTime then @@ -3247,6 +3259,10 @@ function calcs.offence(env, actor, activeSkill) if skillData.hitTimeOverride and not skillData.triggeredOnDeath then output.HitTime = skillData.hitTimeOverride output.HitSpeed = 1 / output.HitTime + elseif skillData.hitRateLimitedByDuration and output.Time and output.Duration and output.Duration > 0 and not skillData.triggeredOnDeath then + -- Recompute from the combined attack time of both weapons when dual wielding + output.HitTime, output.UsesPerHit = calcDurationLimitedHitTime(output.Time, output.Duration) + output.HitSpeed = 1 / output.HitTime elseif skillData.timeOverride and not skillData.triggeredOnDeath then output.Time = skillData.timeOverride elseif skillData.hitTimeMultiplier and output.Time and not skillData.triggeredOnDeath then @@ -3282,6 +3298,19 @@ function calcs.offence(env, actor, activeSkill) breakdown.HitSpeed = { } t_insert(breakdown.HitSpeed, s_format("1 / %.2f ^8(hit time override)", output.HitTime)) t_insert(breakdown.HitSpeed, s_format("= %.2f", output.HitSpeed)) + elseif skillData.hitRateLimitedByDuration and output.HitSpeed and not skillData.triggeredOnDeath then + breakdown.HitTime = { } + local useName = isAttack and "attack" or "cast" + local usesPerHit = output.UsesPerHit or 1 + t_insert(breakdown.HitTime, s_format("%.3f ^8(%s time)", output.Time, useName)) + t_insert(breakdown.HitTime, s_format("x %d ^8(%ss per hit: ceil(%.3f skill duration / %.3f %s time))", usesPerHit, useName, globalOutput.Duration, output.Time, useName)) + if usesPerHit > 1 then + t_insert(breakdown.HitTime, s_format("^8(a %s landing before the previous effect ends creates no new one)", useName)) + end + t_insert(breakdown.HitTime, s_format("= %.3f", output.HitTime)) + breakdown.HitSpeed = { } + t_insert(breakdown.HitSpeed, s_format("1 / %.2f ^8(hit time)", output.HitTime)) + t_insert(breakdown.HitSpeed, s_format("= %.2f", output.HitSpeed)) elseif skillData.hitTimeMultiplier and output.Time and not skillData.triggeredOnDeath then breakdown.HitTime = { } if m_floor(skillData.hitTimeMultiplier) ~= skillData.hitTimeMultiplier then @@ -4062,7 +4091,16 @@ function calcs.offence(env, actor, activeSkill) output.DoubleDamageEffect = output.DoubleDamageChance / 100 output.ScaledDamageEffect = output.ScaledDamageEffect * (1 + output.DoubleDamageEffect + output.TripleDamageEffect) - output.DpsMultiplier = ( skillData.dpsMultiplier or 1 ) * calcLib.mod(skillModList, skillCfg, "DPS") + -- Calculate chance for slam skills to cause an additional aftershock, which repeats the hit + -- Chance is not capped: each full 100% is a guaranteed aftershock and the remainder is a chance for one more + -- Some slams have a separate stat set for the aftershock (e.g. Earthquake): only that set can cause additional aftershocks + output.AftershockEffect = 1 + if activeSkill.skillTypes[SkillType.Slam] and not skillData.cannotCauseAftershock then + output.AftershockChance = skillModList:Sum("BASE", cfg, "AftershockChance") + output.AftershockEffect = 1 + output.AftershockChance / 100 + end + + output.DpsMultiplier = ( skillData.dpsMultiplier or 1 ) * calcLib.mod(skillModList, skillCfg, "DPS") * output.AftershockEffect local hitRate = output.HitChance / 100 * (globalOutput.HitSpeed or globalOutput.Speed) * output.DpsMultiplier @@ -4728,6 +4766,8 @@ function calcs.offence(env, actor, activeSkill) combineStat("AverageDamage", "DPS") combineStat("PvpAverageDamage", "DPS") combineStat("DpsMultiplier", "DPS") + combineStat("AftershockChance", "AVERAGE") + combineStat("AftershockEffect", "AVERAGE") combineStat("TotalDPS", "DPS") combineStat("PvpTotalDPS", "DPS") combineStat("LifeLeechDuration", "DPS") @@ -4872,8 +4912,13 @@ function calcs.offence(env, actor, activeSkill) output.HitSpeed and s_format("x %.2f ^8(hit rate)", output.HitSpeed) or s_format("x %.2f ^8(cast rate)", output.Speed), } end - if output.DpsMultiplier ~= 1 then - t_insert(breakdown.TotalDPS, s_format("x %g ^8(DPS multiplier for this skill)", output.DpsMultiplier)) + local dpsMultiplier = output.DpsMultiplier + if output.AftershockEffect and output.AftershockEffect ~= 1 then + t_insert(breakdown.TotalDPS, s_format("x %.2f ^8(%d%% chance to cause an additional aftershock)", output.AftershockEffect, output.AftershockChance)) + dpsMultiplier = round(dpsMultiplier / output.AftershockEffect, 4) + end + if dpsMultiplier ~= 1 then + t_insert(breakdown.TotalDPS, s_format("x %g ^8(DPS multiplier for this skill)", dpsMultiplier)) end if quantityMultiplier > 1 then t_insert(breakdown.TotalDPS, s_format("x %g ^8(quantity multiplier for this skill)", quantityMultiplier)) diff --git a/src/Modules/CalcSections.lua b/src/Modules/CalcSections.lua index e5604958c8..7273df86c5 100644 --- a/src/Modules/CalcSections.lua +++ b/src/Modules/CalcSections.lua @@ -718,6 +718,7 @@ return { { label = "Rage Cost", color = colorCodes.RAGE, haveOutput = "RageHasCost", { format = "{0:output:RageCost}", { breakdown = "RageCost" }, { modName = { "RageCost", "Cost", "RageNoMult" }, cfg = "skill" }, }, }, { label = "Rage per second", color = colorCodes.RAGE, haveOutput = "RagePerSecondHasCost", { format = "{2:output:RagePerSecondCost}", { breakdown = "RagePerSecondCost" }, { modName = { "RageCost", "Cost", "RageNoMult" }, cfg = "skill" }, }, }, { label = "Armour Break / hit", haveOutput = "ArmourBreakPerHit", { format = "{0:output:ArmourBreakPerHit}", { modName = "ArmourBreakPerHit", modType = "BASE"} }, }, + { label = "Aftershock Chance", haveOutput = "AftershockChance", { format = "{0:output:AftershockChance}%", { modName = "AftershockChance", cfg = "skill" }, }, }, { label = "Soul Cost", color = colorCodes.RAGE, haveOutput = "SoulHasCost", { format = "{0:output:SoulCost}", { breakdown = "SoulCost" }, { modName = { "SoulCost" }, cfg = "skill" }, }, }, { label = "Active Minion Limit", haveOutput = "ActiveMinionLimit", { format = "{0:output:ActiveMinionLimit}", { breakdown = "ActiveMinionLimit" }, diff --git a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua index 6d8d909dcb..64d8691488 100644 --- a/src/Modules/ModParser.lua +++ b/src/Modules/ModParser.lua @@ -2702,6 +2702,11 @@ local specialModList = { flag("AncestralEmpowerment", { type = "SkillType", skillType = SkillType.Slam }), flag("Condition:AncestrallyBoosted", { type = "SkillType", skillType = SkillType.Slam }), }, + -- Aftershocks + ["(%d+)%% chance for slam skills to cause an additional aftershock"] = function(num) return { mod("AftershockChance", "BASE", num, { type = "SkillType", skillType = SkillType.Slam }) } end, + ["(%d+)%% chance for slam skills you use yourself to cause an additional aftershock"] = function(num) return { mod("AftershockChance", "BASE", num, { type = "SkillType", skillType = SkillType.Slam }) } end, -- Earthbreaker (Titan Ascendancy) + ["(%d+)%% chance for mace slam skills you use yourself to cause an additional aftershock"] = function(num) return { mod("AftershockChance", "BASE", num, nil, ModFlag.Mace, 0, { type = "SkillType", skillType = SkillType.Slam }) } end, + ["(%d+)%% chance for shapeshift slam skills you use yourself to cause an additional aftershock"] = function(num) return { mod("AftershockChance", "BASE", num, { type = "SkillType", skillType = SkillType.Slam }, { type = "SkillType", skillType = SkillType.Shapeshift }) } end, -- Leech Related ["life leech is instant"] = { mod("InstantLifeLeech", "BASE", 100), }, ["mana leech is instant"] = { mod("InstantManaLeech", "BASE", 100), },