From df64719b4f44f71204a1f0a4fbd7e104332b2eb4 Mon Sep 17 00:00:00 2001 From: Kyle Anderson Date: Sun, 23 Aug 2026 01:06:45 +0700 Subject: [PATCH] Add support for Cadiro's Gambit --- spec/System/TestCadirosGambit_spec.lua | 272 +++++++++++++++++++++++++ src/Data/ModCache.lua | 2 +- src/Modules/CalcOffence.lua | 1 + src/Modules/CalcSections.lua | 1 + src/Modules/ConfigOptions.lua | 22 ++ src/Modules/ModParser.lua | 27 +++ 6 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 spec/System/TestCadirosGambit_spec.lua diff --git a/spec/System/TestCadirosGambit_spec.lua b/spec/System/TestCadirosGambit_spec.lua new file mode 100644 index 0000000000..7fd3808384 --- /dev/null +++ b/spec/System/TestCadirosGambit_spec.lua @@ -0,0 +1,272 @@ +describe("Cadiro's Gambit", function() + local function equipCadirosGambit() + build.itemsTab:CreateDisplayItemFromRaw([[ + Rarity: Unique + Cadiro's Gambit + Primed Quiver + Implicits: 1 + 10% increased Attack Speed + Each Arrow fired is a Crescendo, Splinter, Reversing, Diamond, Covetous, or Blunt Arrow + ]]) + + local item = build.itemsTab.displayItem + build.itemsTab:AddDisplayItem(true) + build.itemsTab:EquipItemInSet(item, build.itemsTab.activeItemSetId) + return item + end + + local function setupSkill(skillName) + build.itemsTab:CreateDisplayItemFromRaw([[ + New Item + Crude Bow + Quality: 0 + ]]) + build.itemsTab:AddDisplayItem() + build.skillsTab:PasteSocketGroup(skillName .. " 1/0 1") + end + + local arrowConditions = { + "PerandusArrowCrescendo", + "PerandusArrowSplinter", + "PerandusArrowReversing", + "PerandusArrowDiamond", + "PerandusArrowCovetous", + "PerandusArrowBlunt", + } + + local function selectArrow(condition) + build.configTab.input.cadirosGambitArrow = condition + build.configTab:BuildModList() + runCallback("OnFrame") + end + + before_each(function() + newBuild() + runCallback("OnFrame") + end) + + it("only shows its arrow selector while equipped", function() + setupSkill("Shockchain Arrow") + runCallback("OnFrame") + + local control = build.configTab.varControls.cadirosGambitArrow + assert.is_not_nil(control) + assert.is_false(control.shown()) + assert.are.equals("NONE", build.configTab.input.cadirosGambitArrow) + + local cadirosGambit = equipCadirosGambit() + runCallback("OnFrame") + + assert.is_true(control.shown()) + + build.itemsTab:DeleteItem(cadirosGambit) + runCallback("OnFrame") + + assert.is_false(control.shown()) + end) + + it("applies only the selected arrow condition", function() + equipCadirosGambit() + runCallback("OnFrame") + + local selections = { "NONE" } + for _, condition in ipairs(arrowConditions) do + table.insert(selections, condition) + end + + for _, selected in ipairs(selections) do + build.configTab.input.cadirosGambitArrow = selected + build.configTab:BuildModList() + + for _, condition in ipairs(arrowConditions) do + local conditionExists = build.configTab.modList:HasMod( + "FLAG", nil, "Condition:" .. condition + ) + assert.are.equals(selected == condition, conditionExists) + end + end + end) + + it("applies Crescendo Arrow chain and damage bonuses", function() + setupSkill("Shockchain Arrow") + equipCadirosGambit() + + build.configTab.input.skillChainCount = 1 + selectArrow("NONE") + + local baselineChainMax = build.calcsTab.mainOutput.ChainMax or 0 + local baselineSkill = build.calcsTab.mainEnv.player.mainSkill + local baselineDamage = baselineSkill.skillModList:Sum( + "INC", baselineSkill.skillCfg, "Damage" + ) + + selectArrow("PerandusArrowCrescendo") + + local mainSkill = build.calcsTab.mainEnv.player.mainSkill + assert.are.equals(baselineChainMax + 6, build.calcsTab.mainOutput.ChainMax) + assert.are.equals(1, build.calcsTab.mainOutput.Chain) + assert.are.equals(baselineDamage + 60, mainSkill.skillModList:Sum( + "INC", mainSkill.skillCfg, "Damage" + )) + end) + + it("applies Splinter Arrow split bonus", function() + setupSkill("Shockchain Arrow") + equipCadirosGambit() + + selectArrow("NONE") + local baselineSplitCount = build.calcsTab.mainOutput.SplitCount or 0 + + selectArrow("PerandusArrowSplinter") + + assert.are.equals( + baselineSplitCount + 6, + build.calcsTab.mainOutput.SplitCount + ) + assert.are.equals( + baselineSplitCount + 6, + build.calcsTab.mainOutput.SplitCountString + ) + end) + + it("applies Diamond Arrow critical bonuses", function() + setupSkill("Shockchain Arrow") + equipCadirosGambit() + + selectArrow("NONE") + local baselineCritMultiplier = build.calcsTab.mainOutput.CritMultiplier + local baselineSkill = build.calcsTab.mainEnv.player.mainSkill + local baselineCritMultiplierIncrease = baselineSkill.skillModList:Sum( + "INC", baselineSkill.skillCfg, "CritMultiplier" + ) + + selectArrow("PerandusArrowDiamond") + + local mainSkill = build.calcsTab.mainEnv.player.mainSkill + assert.are.equals(100, build.calcsTab.mainOutput.CritChance) + assert.are.equals( + baselineCritMultiplierIncrease + 60, + mainSkill.skillModList:Sum( + "INC", mainSkill.skillCfg, "CritMultiplier" + ) + ) + assert.is_true( + build.calcsTab.mainOutput.CritMultiplier > baselineCritMultiplier + ) + end) + + it("applies Covetous Arrow rarity bonus", function() + setupSkill("Shockchain Arrow") + equipCadirosGambit() + + selectArrow("NONE") + local baselineSkill = build.calcsTab.mainEnv.player.mainSkill + local baselineRarity = baselineSkill.skillModList:Sum( + "INC", baselineSkill.skillCfg, "LootRarity" + ) + + selectArrow("PerandusArrowCovetous") + + local mainSkill = build.calcsTab.mainEnv.player.mainSkill + assert.are.equals( + baselineRarity + 600, + mainSkill.skillModList:Sum( + "INC", mainSkill.skillCfg, "LootRarity" + ) + ) + end) + + it("applies Blunt Arrow stun buildup bonus", function() + setupSkill("Shockchain Arrow") + equipCadirosGambit() + + selectArrow("NONE") + local baselineBuildup = build.calcsTab.mainOutput.HeavyStunBuildupAvg or 0 + local baselineSkill = build.calcsTab.mainEnv.player.mainSkill + local baselineBuildupIncrease = baselineSkill.skillModList:Sum( + "INC", baselineSkill.skillCfg, "EnemyHeavyStunBuildup" + ) + + selectArrow("PerandusArrowBlunt") + + local mainSkill = build.calcsTab.mainEnv.player.mainSkill + assert.are.equals( + baselineBuildupIncrease + 600, + mainSkill.skillModList:Sum( + "INC", mainSkill.skillCfg, "EnemyHeavyStunBuildup" + ) + ) + assert.is_true( + build.calcsTab.mainOutput.HeavyStunBuildupAvg > baselineBuildup + ) + end) + + it("does not apply arrow bonuses to a non-arrow skill", function() + setupSkill("Fireball") + equipCadirosGambit() + + local scopedMods = { + { + condition = "PerandusArrowCrescendo", + modType = "BASE", + modName = "ChainCountMax", + }, + { + condition = "PerandusArrowSplinter", + modType = "BASE", + modName = "SplitCount", + }, + { + condition = "PerandusArrowReversing", + modType = "BASE", + modName = "ProjectileReturnChance", + }, + { + condition = "PerandusArrowDiamond", + modType = "INC", + modName = "CritMultiplier", + }, + { + condition = "PerandusArrowCovetous", + modType = "INC", + modName = "LootRarity", + }, + { + condition = "PerandusArrowBlunt", + modType = "INC", + modName = "EnemyHeavyStunBuildup", + }, + } + + for _, scopedMod in ipairs(scopedMods) do + selectArrow(scopedMod.condition) + + local mainSkill = build.calcsTab.mainEnv.player.mainSkill + assert.are.equals( + 0, + mainSkill.skillModList:Sum( + scopedMod.modType, + mainSkill.skillCfg, + scopedMod.modName + ) + ) + end + end) + + it("shows Reversing Arrow outputs and removes them with the item", function() + setupSkill("Shockchain Arrow") + local cadirosGambit = equipCadirosGambit() + + selectArrow("PerandusArrowReversing") + + assert.are.equals(100, build.calcsTab.mainOutput.PierceCount) + assert.are.equals("All targets", build.calcsTab.mainOutput.PierceCountString) + assert.are.equals(100, build.calcsTab.mainOutput.ProjectileReturnChance) + + build.itemsTab:DeleteItem(cadirosGambit) + runCallback("OnFrame") + + assert.are.equals(0, build.calcsTab.mainOutput.PierceCount) + assert.are.equals(0, build.calcsTab.mainOutput.ProjectileReturnChance) + end) +end) diff --git a/src/Data/ModCache.lua b/src/Data/ModCache.lua index 96c7f933cb..04aea22bd9 100644 --- a/src/Data/ModCache.lua +++ b/src/Data/ModCache.lua @@ -5226,7 +5226,7 @@ c["Double Stun Threshold while Shield is Raised"]={{[1]={[1]={globalLimit=100,gl c["Double the number of your Poisons that targets can be affected by at the same time"]={{[1]={flags=0,keywordFlags=0,name="PoisonCanStack",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="PoisonStacks",type="MORE",value=100}},nil} c["Drop Ignited Ground while moving, which lasts 8 seconds and Ignites as though dealing Fire Damage equal to 10% of your maximum Life"]={nil,"Drop Ignited Ground while moving, which lasts 8 seconds and Ignites as though dealing Fire Damage equal to 10% of your maximum Life "} c["Drop Shocked Ground while moving, lasting 8 seconds"]={nil,"Drop Shocked Ground while moving, lasting 8 seconds "} -c["Each Arrow fired is a Crescendo, Splinter, Reversing, Diamond, Covetous, or Blunt Arrow"]={nil,"Each Arrow fired is a Crescendo, Splinter, Reversing, Diamond, Covetous, or Blunt Arrow "} +c["Each Arrow fired is a Crescendo, Splinter, Reversing, Diamond, Covetous, or Blunt Arrow"]={{[1]={flags=0,keywordFlags=0,name="Condition:HaveCadirosGambit",type="FLAG",value=true},[2]={[1]={type="Condition",var="PerandusArrowCrescendo"},flags=0,keywordFlags=2048,name="ChainCountMax",type="BASE",value=6},[3]={[1]={stat="Chain",type="PerStat"},[2]={type="Condition",var="PerandusArrowCrescendo"},flags=0,keywordFlags=2048,name="Damage",type="INC",value=60},[4]={[1]={type="Condition",var="PerandusArrowSplinter"},flags=0,keywordFlags=2048,name="SplitCount",type="BASE",value=6},[5]={[1]={type="Condition",var="PerandusArrowReversing"},flags=0,keywordFlags=2048,name="PierceAllTargets",type="FLAG",value=true},[6]={[1]={type="Condition",var="PerandusArrowReversing"},flags=0,keywordFlags=2048,name="ProjectileReturnChance",type="BASE",value=100},[7]={[1]={type="Condition",var="PerandusArrowDiamond"},flags=0,keywordFlags=2048,name="CritChance",type="OVERRIDE",value=100},[8]={[1]={type="Condition",var="PerandusArrowDiamond"},flags=0,keywordFlags=2048,name="CritMultiplier",type="INC",value=60},[9]={[1]={type="Condition",var="PerandusArrowCovetous"},flags=0,keywordFlags=2048,name="LootRarity",type="INC",value=600},[10]={[1]={type="Condition",var="PerandusArrowBlunt"},flags=0,keywordFlags=2048,name="EnemyHeavyStunBuildup",type="INC",value=600}},nil} c["Each Totem applies 2% increased Damage taken to Enemies in their Presence"]={{[1]={flags=0,keywordFlags=0,name="EnemyModifier",type="LIST",value={mod={[1]={type="Multiplier",var="TotemsSummoned"},flags=0,keywordFlags=0,name="DamageTaken",type="INC",value=2}}}},nil} c["Echoed Spells have 25% increased Area of Effect"]={nil,"Echoed Spells have 25% increased Area of Effect "} c["Effect is not removed when Unreserved Life is Filled"]={nil,"Effect is not removed when Unreserved Life is Filled "} diff --git a/src/Modules/CalcOffence.lua b/src/Modules/CalcOffence.lua index b6de2bf85d..ee1eb7c772 100644 --- a/src/Modules/CalcOffence.lua +++ b/src/Modules/CalcOffence.lua @@ -1358,6 +1358,7 @@ function calcs.offence(env, actor, activeSkill) breakdown.ProjectileSpeedMod = breakdown.mod(skillModList, skillCfg, "ProjectileSpeed") end output.TwoAdditionalProjectiles = m_min(skillModList:Sum("BASE", skillCfg, "TwoAdditionalProjectilesChance"), 100) + output.ProjectileReturnChance = m_min(skillModList:Sum("BASE", skillCfg, "ProjectileReturnChance"), 100) end if skillFlags.melee then if skillFlags.weapon1Attack then diff --git a/src/Modules/CalcSections.lua b/src/Modules/CalcSections.lua index d885ad0c89..07f6bb1ae5 100644 --- a/src/Modules/CalcSections.lua +++ b/src/Modules/CalcSections.lua @@ -710,6 +710,7 @@ return { { label = "Repeat Count", haveOutput = "RepeatCount", { format = "{output:Repeats}", { modName = { "RepeatCount" }, cfg = "skill" }, }, }, { label = "Projectile Count", flag = "projectile", { format = "{output:ProjectileCount}", { modName = { "NoAdditionalProjectiles" , "ProjectileCount", "SurpassingProjectileChance", "TwoAdditionalProjectilesChance" }, cfg = "skill" }, }, }, { label = "2 Add. Proj. Chance", haveOutput = "TwoAdditionalProjectiles", { format = "{output:TwoAdditionalProjectiles}%", { modName = { "TwoAdditionalProjectilesChance", "NoAdditionalProjectiles" }, cfg = "skill" }, }, }, + { label = "Return Chance", haveOutput = "ProjectileReturnChance", { format = "{0:output:ProjectileReturnChance}%", { modName = "ProjectileReturnChance", modType = "BASE", cfg = "skill" }, }, }, { label = "Pierce Count", haveOutput = "PierceCount", { format = "{output:PierceCountString}", { modName = { "CannotPierce", "PierceCount", "PierceChance", "PierceAllTargets" }, cfg = "skill" }, }, }, { label = "Fork Count", haveOutput = "ForkCountMax", { format = "{output:ForkCountString}", { modName = { "CannotFork", "ForkCountMax" }, cfg = "skill" }, }, }, { label = "Max Chain Count", haveOutput = "ChainMax", { format = "{output:ChainMaxString}", { modName = { "CannotChain", "ChainCountMax", "ChainChance", "NoAdditionalChains" }, cfg = "skill" }, }, }, diff --git a/src/Modules/ConfigOptions.lua b/src/Modules/ConfigOptions.lua index 0437732526..ebe9c6407a 100644 --- a/src/Modules/ConfigOptions.lua +++ b/src/Modules/ConfigOptions.lua @@ -1581,6 +1581,28 @@ Huge sets the radius to 11. modList:NewMod("Keystone", "LIST", "Resolute Technique", "Config") end end }, + { var = "cadirosGambitArrow", type = "list", label = "Cadiro's Gambit Arrow:", ifFlag = "Condition:HaveCadirosGambit", + list = { + { val = "NONE", label = "Not selected", tooltip = "No Cadiro's Gambit arrow effect." }, + { val = "PerandusArrowCrescendo", label = "Crescendo", tooltip = "Crescendo Arrows Chain 6 additional times and deal 60% increased Damage for each time they have Chained." }, + { val = "PerandusArrowSplinter", label = "Splinter", tooltip = "Splintering Arrows Split towards 6 targets." }, + { val = "PerandusArrowReversing", label = "Reversing", tooltip = "Reversing Arrows Pierce all targets and Return to you." }, + { val = "PerandusArrowDiamond", label = "Diamond", tooltip = "All Hits with Diamond Arrows are Critical Hits with 60% increased Critical Damage Bonus." }, + { val = "PerandusArrowCovetous", label = "Covetous", tooltip = "Enemies killed with Covetous Arrows drop items with 600% increased Rarity." }, + { val = "PerandusArrowBlunt", label = "Blunt", tooltip = "Blunt Arrows cause 600% increased Stun Buildup." }, + }, + defaultIndex = 1, + tooltipFunc = function(tooltip, mode, index, value) + tooltip:Clear() + if value and value.tooltip then + tooltip:AddLine(14, value.tooltip) + end + end, + apply = function(val, modList, enemyModList) + if val ~= "NONE" then + modList:NewMod("Condition:" .. val, "FLAG", true, "Config", { type = "Condition", var = "Combat" }) + end + end }, { var = "conditionHaveVulconus", type = "check", label = "Do you have Avatar Of Fire?", ifFlag = "Condition:HaveVulconus", tooltip = "This option is specific to Vulconus.", apply = function(val, modList, enemyModList) modList:NewMod("Condition:HaveAvatarOfFire", "FLAG", true, "Config", { type = "Condition", var = "Combat" }) modList:NewMod("Keystone", "LIST", "Avatar of Fire", "Config") diff --git a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua index 52fa3b4316..ad186d858d 100644 --- a/src/Modules/ModParser.lua +++ b/src/Modules/ModParser.lua @@ -4824,6 +4824,33 @@ local specialModList = { mod("MinionModifier", "LIST", { mod = mod("Speed", "INC", num, nil, ModFlag.Attack, { type = "Multiplier", var = "RageEffect", actor = "player" }) }), } end, -- Projectiles + ["each arrow fired is a crescendo, splinter, reversing, diamond, covetous, or blunt arrow"] = { flag("Condition:HaveCadirosGambit"), + -- Crescendo + mod("ChainCountMax", "BASE", 6, nil, 0, KeywordFlag.Arrow, + { type = "Condition", var = "PerandusArrowCrescendo" }), + mod("Damage", "INC", 60, nil, 0, KeywordFlag.Arrow, + { type = "PerStat", stat = "Chain" }, + { type = "Condition", var = "PerandusArrowCrescendo" }), + -- Splinter + mod("SplitCount", "BASE", 6, nil, 0, KeywordFlag.Arrow, + { type = "Condition", var = "PerandusArrowSplinter" }), + -- Reversing + flag("PierceAllTargets", nil, 0, KeywordFlag.Arrow, + { type = "Condition", var = "PerandusArrowReversing" }), + mod("ProjectileReturnChance", "BASE", 100, nil, 0, KeywordFlag.Arrow, + { type = "Condition", var = "PerandusArrowReversing" }), + -- Diamond + mod("CritChance", "OVERRIDE", 100, nil, 0, KeywordFlag.Arrow, + { type = "Condition", var = "PerandusArrowDiamond" }), + mod("CritMultiplier", "INC", 60, nil, 0, KeywordFlag.Arrow, + { type = "Condition", var = "PerandusArrowDiamond" }), + -- Covetous + mod("LootRarity", "INC", 600, nil, 0, KeywordFlag.Arrow, + { type = "Condition", var = "PerandusArrowCovetous" }), + -- Blunt + mod("EnemyHeavyStunBuildup", "INC", 600, nil, 0, KeywordFlag.Arrow, + { type = "Condition", var = "PerandusArrowBlunt" }), + }, ["skills chain %+(%d) times"] = function(num) return { mod("ChainCountMax", "BASE", num) } end, ["arrows chain %+(%d) times"] = function(num) return { mod("ChainCountMax", "BASE", num, nil, 0, KeywordFlag.Arrow) } end, ["skills chain an additional time while at maximum frenzy charges"] = { mod("ChainCountMax", "BASE", 1, { type = "StatThreshold", stat = "FrenzyCharges", thresholdStat = "FrenzyChargesMax" }) },