From a72e9a2a3496dfdd44e277c8b4dfdc45f372b2f9 Mon Sep 17 00:00:00 2001 From: Mitchel Pigsley Date: Mon, 2 Nov 2026 10:52:46 -0700 Subject: [PATCH 1/2] 5e-srd-5.2: Cunning Strike's cost is a derived parameter, its DC a computed value (#251) Signed-off-by: Mitchel Pigsley --- .changeset/5e-srd-5-2-cunning-strike-cost.md | 5 ++ packs/5e/srd-5.2/README.md | 10 +-- .../features/rogue/cunning-strike.yaml | 65 ++++++++++++------- packs/5e/srd-5.2/i18n/en/class-rogue.yaml | 1 - packs/5e/srd-5.2/rules/class-rogue.yaml | 48 ++------------ 5 files changed, 58 insertions(+), 71 deletions(-) create mode 100644 .changeset/5e-srd-5-2-cunning-strike-cost.md diff --git a/.changeset/5e-srd-5-2-cunning-strike-cost.md b/.changeset/5e-srd-5-2-cunning-strike-cost.md new file mode 100644 index 000000000..e95787eb1 --- /dev/null +++ b/.changeset/5e-srd-5-2-cunning-strike-cost.md @@ -0,0 +1,5 @@ +--- +'@opencharacter/5e-srd-5.2': minor +--- + +Cunning Strike works out its Sneak Attack dice once, as the attack's derived parameter `cunningStrikeCost`, which the Sneak Attack and the strike effects read; its save DC is the feature's `strikeDc` computed value, read by the strike effects. The `rogue.strikeDc` field is removed (breaking: read the feature's `strikeDc` instead). The rules are unchanged. diff --git a/packs/5e/srd-5.2/README.md b/packs/5e/srd-5.2/README.md index e23ce7098..d345e924a 100644 --- a/packs/5e/srd-5.2/README.md +++ b/packs/5e/srd-5.2/README.md @@ -146,9 +146,8 @@ automatic run finishes too; a `level` parameter starts higher. A monster is made Metamagic options, Pact Magic and the 28 invocations, the spellbook, Arcane Recovery. Uses until a rest are state on the class copy, an invocation's on its pick (Gift of the Depths' `used`). Features carry the numbers their card needs as computed values (in computeSheet's - `activationValues`), which their actions read: Cunning Strike's `strikeDc` (its saves' DC; the - strike effects read it from the `rogue.strikeDc` field, since effects can't read computed values - yet ([#251](https://github.com/pixelqueue/opencharacter/issues/251))), Intimidating + `activationValues`), which their actions and action-time effects read: Cunning Strike's + `strikeDc` (its saves' DC, read by the strike effects), Intimidating Presence's `presenceDc`, Relentless Rage's `rageDc`, Sacred Weapon's `weaponBonus`, Preserve Life's `healingPool`, and Breath Weapon's `breathDc` and `breathDice`. A species trait keeps its own uses as its grant's saved state (Breath Weapon, Draconic Flight, @@ -337,8 +336,9 @@ Where the SRD is unclear or silent, the pack reads it this way: of the attack (an `action` effect on its `attack-hit` step), Poisoned, Prone, Unconscious or Blinded on a failure. An effect the rogue can't use (`rogue.strikeEffects`: not known yet, or Poison without a Poisoner's Kit), or effects together costing more dice than the Sneak Attack - has, do nothing and cost nothing, as does a Trip on a Huge or larger target. Withdraw, - Daze's limit and Stealth Attack are the player's. + has, do nothing and cost nothing, as does a Trip on a Huge or larger target. The attack works + out the dice spent once, as the derived parameter `cunningStrikeCost`, from its first target + (an attack has one). Withdraw, Daze's limit and Stealth Attack are the player's. - Stunning Strike (`stunningStrike`, 1 Focus Point, Constitution or Stunned), Open Hand Technique (`openHand`, on an Unarmed Strike: Push's Strength save, Topple's Dexterity save or Prone; it is offered on every Unarmed Strike, and whether the strike is one of Flurry of diff --git a/packs/5e/srd-5.2/content/features/rogue/cunning-strike.yaml b/packs/5e/srd-5.2/content/features/rogue/cunning-strike.yaml index 901c83882..550fd5f7f 100644 --- a/packs/5e/srd-5.2/content/features/rogue/cunning-strike.yaml +++ b/packs/5e/srd-5.2/content/features/rogue/cunning-strike.yaml @@ -1,18 +1,18 @@ id: features.rogue.cunning-strike type: feature -# The save DC (`rogue.strikeDc`): 8 + Dexterity modifier + Proficiency Bonus, on the card too. +# The save DC: 8 + Dexterity modifier + Proficiency Bonus, on the card too. computed: - strikeDc: { type: integer, formula: rogue.strikeDc } + strikeDc: { type: integer, formula: 8 + abilities.dex.mod + proficiency.bonus } # The effect is a parameter of each attack (`cunningStrike`; Improved Cunning Strike adds # `cunningStrike2`). Its dice come off the Sneak Attack (the rules' `sneakAttack` situational -# choice), and on a hit that deals Sneak Attack damage the target saves -# against it. An effect the rogue can't use (not in `rogue.strikeEffects`: one it doesn't know yet, or -# Poison without a Poisoner's Kit), or effects together costing more dice than the Sneak Attack -# has, do nothing and cost nothing; so does a Trip against a Huge or larger target. Withdraw's +# choice reads `cunningStrikeCost`), and on a hit that deals Sneak Attack damage the target saves +# against it. An effect the rogue can't use (not in `rogue.strikeEffects`: one it doesn't know yet, +# or Poison without a Poisoner's Kit), or effects together costing more dice than the Sneak Attack +# has, do nothing and cost nothing; so does a Trip against a Huge or larger target (the cost reads +# the first target's size: one value per attack, and an attack has one target). Withdraw's # movement, Daze's limit and Stealth Attack are the player's. effects: - { target: rogue.cunningStrikes, op: atLeast, value: 1 } - - { target: rogue.strikeDc, op: set, value: 8 + abilities.dex.mod + proficiency.bonus } - target: { actions: [attack-hit] } op: params value: @@ -20,6 +20,37 @@ effects: type: enum values: [poison, trip, withdraw, daze, knock-out, obscure, stealth-attack] optional: true + # The Sneak Attack dice the effects spend: those the rogue can use on this target, if + # together they fit in the Sneak Attack, else none. + cunningStrikeCost: + type: integer + formula: >- + (has(params.cunningStrike) && params.cunningStrike in rogue.strikeEffects + && !(params.cunningStrike == 'trip' && has(participants.target) + && size(participants.target) > 0 && has(participants.target[0].creature.size) + && participants.target[0].creature.size in ['huge', 'gargantuan']) + ? (params.cunningStrike == 'knock-out' ? 6 : params.cunningStrike == 'obscure' ? 3 + : params.cunningStrike == 'daze' ? 2 : 1) : 0) + + (has(params.cunningStrike2) && params.cunningStrike2 in rogue.strikeEffects + && !(params.cunningStrike2 == 'trip' && has(participants.target) + && size(participants.target) > 0 && has(participants.target[0].creature.size) + && participants.target[0].creature.size in ['huge', 'gargantuan']) + ? (params.cunningStrike2 == 'knock-out' ? 6 : params.cunningStrike2 == 'obscure' ? 3 + : params.cunningStrike2 == 'daze' ? 2 : 1) : 0) + <= rogue.sneakAttack + ? (has(params.cunningStrike) && params.cunningStrike in rogue.strikeEffects + && !(params.cunningStrike == 'trip' && has(participants.target) + && size(participants.target) > 0 && has(participants.target[0].creature.size) + && participants.target[0].creature.size in ['huge', 'gargantuan']) + ? (params.cunningStrike == 'knock-out' ? 6 : params.cunningStrike == 'obscure' ? 3 + : params.cunningStrike == 'daze' ? 2 : 1) : 0) + + (has(params.cunningStrike2) && params.cunningStrike2 in rogue.strikeEffects + && !(params.cunningStrike2 == 'trip' && has(participants.target) + && size(participants.target) > 0 && has(participants.target[0].creature.size) + && participants.target[0].creature.size in ['huge', 'gargantuan']) + ? (params.cunningStrike2 == 'knock-out' ? 6 : params.cunningStrike2 == 'obscure' ? 3 + : params.cunningStrike2 == 'daze' ? 2 : 1) : 0) + : 0 - target: { actions: [attack-hit] } op: params when: rogue.cunningStrikes >= 2 @@ -31,13 +62,7 @@ effects: has(params.cunningStrike) && params.cunningStrike in ['poison', 'trip', 'knock-out', 'obscure', 'daze'] && params.cunningStrike in rogue.strikeEffects - && (params.cunningStrike == 'knock-out' ? 6 : params.cunningStrike == 'obscure' ? 3 - : params.cunningStrike == 'daze' ? 2 : 1) - + (has(params.cunningStrike2) && params.cunningStrike2 in rogue.strikeEffects - && !(params.cunningStrike2 == 'trip' && participant.creature.size in ['huge', 'gargantuan']) - ? (params.cunningStrike2 == 'knock-out' ? 6 : params.cunningStrike2 == 'obscure' ? 3 - : params.cunningStrike2 == 'daze' ? 2 : 1) : 0) - <= rogue.sneakAttack + && params.cunningStrikeCost > 0 && situational.sneakAttack && has(action.item.damageType) && (action.item.range == 'ranged' || 'finesse' in action.item.tags) && (params.cunningStrike != 'trip' @@ -49,7 +74,7 @@ effects: as: participant params: ability: "params.cunningStrike in ['trip', 'obscure'] ? 'dex' : 'con'" - dc: rogue.strikeDc + dc: self.strikeDc changes: - type: applyEffect when: run.result == 'failure' && params.cunningStrike != 'daze' @@ -64,13 +89,7 @@ effects: has(params.cunningStrike2) && params.cunningStrike2 in ['poison', 'trip', 'knock-out', 'obscure', 'daze'] && params.cunningStrike2 in rogue.strikeEffects - && (params.cunningStrike2 == 'knock-out' ? 6 : params.cunningStrike2 == 'obscure' ? 3 - : params.cunningStrike2 == 'daze' ? 2 : 1) - + (has(params.cunningStrike) && params.cunningStrike in rogue.strikeEffects - && !(params.cunningStrike == 'trip' && participant.creature.size in ['huge', 'gargantuan']) - ? (params.cunningStrike == 'knock-out' ? 6 : params.cunningStrike == 'obscure' ? 3 - : params.cunningStrike == 'daze' ? 2 : 1) : 0) - <= rogue.sneakAttack + && params.cunningStrikeCost > 0 && situational.sneakAttack && has(action.item.damageType) && (action.item.range == 'ranged' || 'finesse' in action.item.tags) && (params.cunningStrike2 != 'trip' @@ -82,7 +101,7 @@ effects: as: participant params: ability: "params.cunningStrike2 in ['trip', 'obscure'] ? 'dex' : 'con'" - dc: rogue.strikeDc + dc: self.strikeDc changes: - type: applyEffect when: run.result == 'failure' && params.cunningStrike2 != 'daze' diff --git a/packs/5e/srd-5.2/i18n/en/class-rogue.yaml b/packs/5e/srd-5.2/i18n/en/class-rogue.yaml index ab9193db5..2216d4b62 100644 --- a/packs/5e/srd-5.2/i18n/en/class-rogue.yaml +++ b/packs/5e/srd-5.2/i18n/en/class-rogue.yaml @@ -22,7 +22,6 @@ classes.rogue.state.strokeOfLuckUsed.name: 'Stroke of Luck used' fields.rogue.sneakAttack.name: 'Sneak Attack dice (d6)' fields.rogue.cunningStrikes.name: 'Cunning Strike effects per Sneak Attack' -fields.rogue.strikeDc.name: 'Cunning Strike save DC' situational.sneakAttack.name: 'Sneak Attack' situational.uncannyDodge.name: 'Uncanny Dodge (halve this attack’s damage)' diff --git a/packs/5e/srd-5.2/rules/class-rogue.yaml b/packs/5e/srd-5.2/rules/class-rogue.yaml index 9ac17c687..c9fea9a40 100644 --- a/packs/5e/srd-5.2/rules/class-rogue.yaml +++ b/packs/5e/srd-5.2/rules/class-rogue.yaml @@ -3,9 +3,8 @@ fields: rogue.cunningStrikes: { type: integer, formula: 0 } rogue.deviousStrikes: { type: boolean, formula: false } rogue.stealthAttack: { type: boolean, formula: false } - # Cunning Strike: the save DC, and the effects the rogue can use now (Poison needs a Poisoner's - # Kit; Devious Strikes adds Daze, Knock Out and Obscure; Supreme Sneak, Stealth Attack). - rogue.strikeDc: { type: integer, formula: 0 } + # Cunning Strike: the effects the rogue can use now (Poison needs a Poisoner's Kit; Devious + # Strikes adds Daze, Knock Out and Obscure; Supreme Sneak, Stealth Attack). rogue.strikeEffects: type: set of: string @@ -18,8 +17,7 @@ fields: rogue.uncannyDodge: { type: boolean, formula: false } # Cunning Strike's effects (the attack's `cunningStrike` and `cunningStrike2` parameters) spend -# Sneak Attack dice: the ones the rogue can use on this target (Trip needs a Large or smaller one), -# if together they fit in the Sneak Attack (else none is used, and nothing is spent). +# Sneak Attack dice: the attack's `cunningStrikeCost`, worked out by the feature. situational: - id: sneakAttack type: boolean @@ -30,30 +28,7 @@ situational: - target: { rolls: [damage.weapon] } op: add value: >- - (rogue.sneakAttack - ((has(params.cunningStrike) && params.cunningStrike in rogue.strikeEffects - && !(params.cunningStrike == 'trip' && has(participants.target) && size(participants.target) > 0 - && has(participants.target[0].creature.size) - && participants.target[0].creature.size in ['huge', 'gargantuan']) - ? (params.cunningStrike == 'knock-out' ? 6 : params.cunningStrike == 'obscure' ? 3 - : params.cunningStrike == 'daze' ? 2 : 1) : 0) - + (has(params.cunningStrike2) && params.cunningStrike2 in rogue.strikeEffects - && !(params.cunningStrike2 == 'trip' && has(participants.target) && size(participants.target) > 0 - && has(participants.target[0].creature.size) - && participants.target[0].creature.size in ['huge', 'gargantuan']) - ? (params.cunningStrike2 == 'knock-out' ? 6 : params.cunningStrike2 == 'obscure' ? 3 - : params.cunningStrike2 == 'daze' ? 2 : 1) : 0) <= rogue.sneakAttack - ? (has(params.cunningStrike) && params.cunningStrike in rogue.strikeEffects - && !(params.cunningStrike == 'trip' && has(participants.target) && size(participants.target) > 0 - && has(participants.target[0].creature.size) - && participants.target[0].creature.size in ['huge', 'gargantuan']) - ? (params.cunningStrike == 'knock-out' ? 6 : params.cunningStrike == 'obscure' ? 3 - : params.cunningStrike == 'daze' ? 2 : 1) : 0) - + (has(params.cunningStrike2) && params.cunningStrike2 in rogue.strikeEffects - && !(params.cunningStrike2 == 'trip' && has(participants.target) && size(participants.target) > 0 - && has(participants.target[0].creature.size) - && participants.target[0].creature.size in ['huge', 'gargantuan']) - ? (params.cunningStrike2 == 'knock-out' ? 6 : params.cunningStrike2 == 'obscure' ? 3 - : params.cunningStrike2 == 'daze' ? 2 : 1) : 0) : 0))d6 + (rogue.sneakAttack - (has(params.cunningStrikeCost) ? params.cunningStrikeCost : 0))d6 tags: [ { @@ -62,18 +37,8 @@ situational: ] when: >- situational.sneakAttack - && rogue.sneakAttack != (has(params.cunningStrike) && params.cunningStrike in rogue.strikeEffects - && !(params.cunningStrike == 'trip' && has(participants.target) && size(participants.target) > 0 - && has(participants.target[0].creature.size) - && participants.target[0].creature.size in ['huge', 'gargantuan']) - ? (params.cunningStrike == 'knock-out' ? 6 : params.cunningStrike == 'obscure' ? 3 - : params.cunningStrike == 'daze' ? 2 : 1) : 0) - + (has(params.cunningStrike2) && params.cunningStrike2 in rogue.strikeEffects - && !(params.cunningStrike2 == 'trip' && has(participants.target) && size(participants.target) > 0 - && has(participants.target[0].creature.size) - && participants.target[0].creature.size in ['huge', 'gargantuan']) - ? (params.cunningStrike2 == 'knock-out' ? 6 : params.cunningStrike2 == 'obscure' ? 3 - : params.cunningStrike2 == 'daze' ? 2 : 1) : 0) + && rogue.sneakAttack + != (has(params.cunningStrikeCost) ? params.cunningStrikeCost : 0) && has(roll.item) && has(roll.item.damageType) && (roll.item.range == 'ranged' || 'finesse' in roll.item.tags) - id: uncannyDodge @@ -93,6 +58,5 @@ display: { groups: [class-features], priority: 2, showWhen: rogue.cunningStrikes > 0 } rogue.deviousStrikes: { hidden: true } rogue.stealthAttack: { hidden: true } - rogue.strikeDc: { groups: [class-features], priority: 2, showWhen: rogue.cunningStrikes > 0 } rogue.strikeEffects: { hidden: true } rogue.uncannyDodge: { hidden: true } From 2b0f57b175e212db4a74232e4bc65a5595f503ee Mon Sep 17 00:00:00 2001 From: Mitchel Pigsley Date: Mon, 2 Nov 2026 10:52:46 -0700 Subject: [PATCH 2/2] draw-steel-heroes: Healing Grace's enhancements bound spend (#252) Signed-off-by: Mitchel Pigsley --- packs/draw-steel/heroes/README.md | 6 +++--- .../content/abilities/conduit/healing-grace.yaml | 12 +++++------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packs/draw-steel/heroes/README.md b/packs/draw-steel/heroes/README.md index 8e2e8e44e..6b7e1e063 100644 --- a/packs/draw-steel/heroes/README.md +++ b/packs/draw-steel/heroes/README.md @@ -106,9 +106,9 @@ computeSheet(ruleset, made.character).values['draw-steel-heroes:stamina.max']?.v Visceral Roar and Deluge take the stormwight kit's primordial type (`fury.primordialType`), and Deluge's ignores immunity. - Healing Grace's Spend enhancements: `graceRecoveries`, `graceStand` and `graceEnd`, which - must fit in `spend`. `graceEndEntry` is the ID of the applied entry to end on the target; - without it, the first that a saving throw or the end of a turn ends. An overspend is - reported on `graceRecoveries` ([#252](https://github.com/pixelqueue/opencharacter/issues/252)). + must fit in `spend` (they bound it: an overspend is `out-of-range` on `spend`). + `graceEndEntry` is the ID of the applied entry to end on the target; without it, the first + that a saving throw or the end of a turn ends. - No Dying on My Watch's `adjacentEnemies`: the ally gains 5 temporary Stamina for each. An `ally` participant is the ally a triggered ability answers or an ability helps: No Dying on diff --git a/packs/draw-steel/heroes/content/abilities/conduit/healing-grace.yaml b/packs/draw-steel/heroes/content/abilities/conduit/healing-grace.yaml index 59b4b633f..943bfdf02 100644 --- a/packs/draw-steel/heroes/content/abilities/conduit/healing-grace.yaml +++ b/packs/draw-steel/heroes/content/abilities/conduit/healing-grace.yaml @@ -16,16 +16,14 @@ fields: effects: # Spend 1+ piety, one enhancement each: more targets (the participants), a Recovery more for a # target, a prone target standing up, or an effect a saving throw or the end of their turn ends - # (the first such). Pass `spend` for the total: the extra Recoveries, standing and ending fit - # in it. + # (the first such). Pass `spend` for the total: the extra Recoveries, standing and ending + # bound it from below. - target: { actions: [ability], from: self } op: params value: - graceRecoveries: - type: integer - default: 0 - min: 0 - max: 'params.spend - (params.graceStand ? 1 : 0) - (params.graceEnd ? 1 : 0)' + spend: + { min: 'params.graceRecoveries + (params.graceStand ? 1 : 0) + (params.graceEnd ? 1 : 0)' } + graceRecoveries: { type: integer, default: 0, min: 0 } graceStand: { type: boolean, default: false } graceEnd: { type: boolean, default: false } # The applied entry to end (its ID on the target); without it, the first that qualifies.