From 162c74f3ae57429ad3e31ebf1ef5a89705602b95 Mon Sep 17 00:00:00 2001 From: Chris Ryan Date: Tue, 25 Nov 2025 21:44:40 +1000 Subject: [PATCH 01/30] Update the death move descriptions --- lang/en.json | 6 +++--- module/applications/dialogs/deathMove.mjs | 26 +++++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/lang/en.json b/lang/en.json index c99cf652..230e3b6f 100755 --- a/lang/en.json +++ b/lang/en.json @@ -965,15 +965,15 @@ "DeathMoves": { "avoidDeath": { "name": "Avoid Death", - "description": "You drop unconscious temporarily and work with the GM to describe how the situation gets much worse because of it. Then roll your Fear die; if its value is equal to or under your Level, take a Scar." + "description": "Your character avoids death and faces the consequences. They temporarily drop unconscious, and then you work with the GM to describe how the situation worsens. While unconscious, your character can't move or act, and they can't be targeted by an attack. They return to consciousness when an ally clears 1 or more of their marked Hit Points or when the party finishes a long rest. After your character falls unconscious, roll your Hope Die. If its value is equal to or less than your character's level, they gain a scar: permanently cross out a Hope slot and work with the GM to determine its lasting narrative impact and how, if possible, it can be restored. If you ever cross out your last Hope slot, your character's journey ends." }, "riskItAll": { "name": "Risk It All", - "description": "Roll your Duality Dice. If Hope is higher, you stay on your feet and clear an amount of Hit Points and/or Stress equal to the value of the Hope die (divide the Hope die value up between these however you’d prefer). If your Fear die is higher, you cross through the veil of death. If the Duality Dice are tied, you stay on your feet and clear all Hit Points and Stress." + "description": "Roll your Duality Dice. If the Hope Die is higher, your character stays on their feet and clears a number of Hit Points or Stress equal to the value of the Hope Die (you can divide the Hope Die value between Hit Points and Stress however you'd prefer). If the Fear Die is higher, your character crosses through the veil of death. If the Duality Dice show matching results, your character stays up and clears all Hit Points and Stress." }, "blazeOfGlory": { "name": "Blaze Of Glory", - "description": "With Blaze of Glory, the player is accepting death for the character. Take one action (at GM discretion), which becomes an automatic critical success, then cross through the veil of death." + "description": " Your character embraces death and goes out in a blaze of glory. Take one final action. It automatically critically succeeds (with GM approval), and then you cross through the veil of death." } }, "DomainCardTypes": { diff --git a/module/applications/dialogs/deathMove.mjs b/module/applications/dialogs/deathMove.mjs index d0686d2b..78959538 100644 --- a/module/applications/dialogs/deathMove.mjs +++ b/module/applications/dialogs/deathMove.mjs @@ -38,6 +38,20 @@ export default class DhpDeathMove extends HandlebarsApplicationMixin(Application return context; } + async handleAvoidDeath() { + console.log("Avoid Death!"); + if (game.modules.get('dice-so-nice')?.active) { + + const { diceSoNice } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance); + + const hopeDice = await getDiceSoNicePreset(diceSoNice.hope, 12); + } + } + + handleRiskItAll() { + console.log("Risk It All!"); + } + static selectMove(_, button) { const move = button.dataset.move; this.selectedMove = CONFIG.DH.GENERAL.deathMoves[move]; @@ -73,6 +87,18 @@ export default class DhpDeathMove extends HandlebarsApplicationMixin(Application cls.create(msg); + if (CONFIG.DH.GENERAL.deathMoves.avoidDeath === this.selectedMove) { + this.handleAvoidDeath(); + return; + } + + if (CONFIG.DH.GENERAL.deathMoves.riskItAll === this.selectedMove) { + this.handleRiskItAll(); + return; + } + + this.close(); + } } From 04f8793f204761185107af868644485bcf3be0cc Mon Sep 17 00:00:00 2001 From: Chris Ryan Date: Wed, 26 Nov 2025 20:53:21 +1000 Subject: [PATCH 02/30] Renamed to DhDeathMove --- module/applications/dialogs/deathMove.mjs | 2 +- module/applications/sheets/actors/character.mjs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/module/applications/dialogs/deathMove.mjs b/module/applications/dialogs/deathMove.mjs index 78959538..1b85d0d2 100644 --- a/module/applications/dialogs/deathMove.mjs +++ b/module/applications/dialogs/deathMove.mjs @@ -1,6 +1,6 @@ const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; -export default class DhpDeathMove extends HandlebarsApplicationMixin(ApplicationV2) { +export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV2) { constructor(actor) { super({}); diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index c4962b18..8d1ed124 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -1,5 +1,5 @@ import DHBaseActorSheet from '../api/base-actor.mjs'; -import DhpDeathMove from '../../dialogs/deathMove.mjs'; +import DhDeathMove from '../../dialogs/deathMove.mjs'; import { abilities } from '../../../config/actorConfig.mjs'; import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs'; import DhCharacterCreation from '../../characterCreation/characterCreation.mjs'; @@ -669,7 +669,7 @@ export default class CharacterSheet extends DHBaseActorSheet { * @type {ApplicationClickAction} */ static async #makeDeathMove() { - await new DhpDeathMove(this.document).render({ force: true }); + await new DhDeathMove(this.document).render({ force: true }); } /** From 54996e7e12067d20bd7849c235528e4e90a82331 Mon Sep 17 00:00:00 2001 From: Chris Ryan Date: Wed, 26 Nov 2025 23:06:53 +1000 Subject: [PATCH 03/30] Partial Fate Roll creation and Fate Roll Enricher (/fr) --- daggerheart.mjs | 30 ++++- module/applications/dialogs/d20RollDialog.mjs | 4 +- module/data/chat-message/_modules.mjs | 1 + module/dice/_module.mjs | 1 + module/dice/fateRoll.mjs | 127 ++++++++++++++++++ module/documents/chatMessage.mjs | 4 + module/enrichers/FateRollEnricher.mjs | 64 +++++++++ module/enrichers/_module.mjs | 11 +- module/helpers/utils.mjs | 9 +- styles/less/global/enrichment.less | 1 + system.json | 1 + templates/dialogs/dice-roll/rollSelection.hbs | 11 ++ 12 files changed, 257 insertions(+), 7 deletions(-) create mode 100644 module/dice/fateRoll.mjs create mode 100644 module/enrichers/FateRollEnricher.mjs diff --git a/daggerheart.mjs b/daggerheart.mjs index 55a7d0bf..c6c68c8d 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -8,8 +8,9 @@ import * as fields from './module/data/fields/_module.mjs'; import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs'; import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs'; import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs'; -import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll } from './module/dice/_module.mjs'; +import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll, FateRoll } from './module/dice/_module.mjs'; import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs'; +import { enrichedFateRoll } from './module/enrichers/FateRollEnricher.mjs'; import { handlebarsRegistration, runMigrations, @@ -24,12 +25,13 @@ import TemplateManager from './module/documents/templateManager.mjs'; CONFIG.DH = SYSTEM; CONFIG.TextEditor.enrichers.push(...enricherConfig); -CONFIG.Dice.rolls = [BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll]; +CONFIG.Dice.rolls = [BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll, FateRoll]; CONFIG.Dice.daggerheart = { DHRoll: DHRoll, DualityRoll: DualityRoll, D20Roll: D20Roll, - DamageRoll: DamageRoll + DamageRoll: DamageRoll, + FateRoll: FateRoll }; CONFIG.Actor.documentClass = documents.DhpActor; @@ -240,6 +242,28 @@ Hooks.on('chatMessage', (_, message) => { }); return false; } + + if (message.startsWith('/fr')) { + const result = + message.trim().toLowerCase() === '/fr' ? { result: {} } : rollCommandToJSON(message.replace(/\/fr\s?/, '')); + if (!result) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateParsing')); + return false; + } + + const { result: rollCommand, flavor } = result; + + const target = getCommandTarget({ allowNull: true }); + const title = 'Fate'; + + enrichedFateRoll({ + target, + title, + label: 'test', + }); + return false; + } + }); Hooks.on('moveToken', async (movedToken, data) => { diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index 2534a2b8..52b50c95 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -116,14 +116,14 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio context.isLite = this.config.roll?.lite; context.extraFormula = this.config.extraFormula; context.formula = this.roll.constructFormula(this.config); - if (this.actor.system.traits) context.abilities = this.getTraitModifiers(); + if (this.actor?.system?.traits) context.abilities = this.getTraitModifiers(); context.showReaction = !this.config.roll?.type && context.rollType === 'DualityRoll'; context.reactionOverride = this.reactionOverride; } const tagTeamSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); - if (tagTeamSetting.members[this.actor.id] && !this.config.skips?.createMessage) { + if (this.actor?.id && tagTeamSetting.members[this.actor.id] && !this.config.skips?.createMessage) { context.activeTagTeamRoll = true; context.tagTeamSelected = this.config.tagTeamSelected; } diff --git a/module/data/chat-message/_modules.mjs b/module/data/chat-message/_modules.mjs index ec095aac..c671de31 100644 --- a/module/data/chat-message/_modules.mjs +++ b/module/data/chat-message/_modules.mjs @@ -8,6 +8,7 @@ export const config = { adversaryRoll: DHActorRoll, damageRoll: DHActorRoll, dualityRoll: DHActorRoll, + fateRoll: DHActorRoll, groupRoll: DHGroupRoll, systemMessage: DHSystemMessage }; diff --git a/module/dice/_module.mjs b/module/dice/_module.mjs index e6755a74..b9339d87 100644 --- a/module/dice/_module.mjs +++ b/module/dice/_module.mjs @@ -3,3 +3,4 @@ export { default as D20Roll } from './d20Roll.mjs'; export { default as DamageRoll } from './damageRoll.mjs'; export { default as DHRoll } from './dhRoll.mjs'; export { default as DualityRoll } from './dualityRoll.mjs'; +export { default as FateRoll } from './fateRoll.mjs'; diff --git a/module/dice/fateRoll.mjs b/module/dice/fateRoll.mjs new file mode 100644 index 00000000..616a11db --- /dev/null +++ b/module/dice/fateRoll.mjs @@ -0,0 +1,127 @@ +import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; +import D20Roll from './d20Roll.mjs'; +import { setDiceSoNiceForFateRoll } from '../helpers/utils.mjs'; + +export default class FateRoll extends D20Roll { + constructor(formula, data = {}, options = {}) { + super(formula, data, options); + } + + static messageType = 'fateRoll'; + + static DefaultDialog = D20RollDialog; + + get title() { + return game.i18n.localize( + `DAGGERHEART.GENERAL.fateRoll` + ); + } + + get dHope() { + // if ( !(this.terms[0] instanceof foundry.dice.terms.Die) ) return; + if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); + return this.dice[0]; + // return this.#hopeDice; + } + + set dHope(faces) { + if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); + this.terms[0].faces = this.getFaces(faces); + // this.#hopeDice = `d${face}`; + } + + get isCritical() { + return false; + } + + static getHooks(hooks) { + return [...(hooks ?? []), 'Fate']; + } + + /** @inheritDoc */ + static fromData(data) { + data.terms[0].class = foundry.dice.terms.Die.name; + return super.fromData(data); + } + + createBaseDice() { + if (this.dice[0] instanceof foundry.dice.terms.Die) { + this.terms = [this.terms[0]]; + return; + } + this.terms[0] = new foundry.dice.terms.Die({ faces: 12 }); + } + + static async buildEvaluate(roll, config = {}, message = {}) { + await super.buildEvaluate(roll, config, message); + + await setDiceSoNiceForFateRoll( + roll, + config.roll.hope.dice + ); + } + + static postEvaluate(roll, config = {}) { + const data = super.postEvaluate(roll, config); + + data.hope = { + dice: roll.dHope.denomination, + value: roll.dHope.total, + rerolled: { + any: roll.dHope.results.some(x => x.rerolled), + rerolls: roll.dHope.results.filter(x => x.rerolled) + } + }; + + return data; + } + + // static async reroll(rollString, target, message) { + // let parsedRoll = game.system.api.dice.DualityRoll.fromData({ ...rollString, evaluated: false }); + // const term = parsedRoll.terms[target.dataset.dieIndex]; + // await term.reroll(`/r1=${term.total}`); + // if (game.modules.get('dice-so-nice')?.active) { + // const diceSoNiceRoll = { + // _evaluated: true, + // dice: [ + // new foundry.dice.terms.Die({ + // ...term, + // faces: term._faces, + // results: term.results.filter(x => !x.rerolled) + // }) + // ], + // options: { appearance: {} } + // }; + + // const diceSoNicePresets = await getDiceSoNicePresets(`d${term._faces}`, `d${term._faces}`); + // const type = target.dataset.type; + // if (diceSoNicePresets[type]) { + // diceSoNiceRoll.dice[0].options = diceSoNicePresets[type]; + // } + + // await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true); + // } + + // await parsedRoll.evaluate(); + + // const newRoll = game.system.api.dice.DualityRoll.postEvaluate(parsedRoll, { + // targets: message.system.targets, + // roll: { + // advantage: message.system.roll.advantage?.type, + // difficulty: message.system.roll.difficulty ? Number(message.system.roll.difficulty) : null + // } + // }); + // newRoll.extra = newRoll.extra.slice(2); + + // const tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); + // Hooks.call(`${CONFIG.DH.id}.postRollDuality`, { + // source: { actor: message.system.source.actor ?? '' }, + // targets: message.system.targets, + // tagTeamSelected: Object.values(tagTeamSettings.members).some(x => x.messageId === message._id), + // roll: newRoll, + // rerolledRoll: + // newRoll.result.duality !== message.system.roll.result.duality ? message.system.roll : undefined + // }); + // return { newRoll, parsedRoll }; + // } +} diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index 7e313891..46383c3a 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -87,6 +87,10 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { break; } } + if (this.type === 'fateRoll') { + html.classList.add('fate'); + html.classList.add('hope'); + } const autoExpandRoll = game.settings.get( CONFIG.DH.id, diff --git a/module/enrichers/FateRollEnricher.mjs b/module/enrichers/FateRollEnricher.mjs new file mode 100644 index 00000000..0fb4f335 --- /dev/null +++ b/module/enrichers/FateRollEnricher.mjs @@ -0,0 +1,64 @@ +import { abilities } from '../config/actorConfig.mjs'; +import { getCommandTarget, rollCommandToJSON } from '../helpers/utils.mjs'; + +export default function DhFateRollEnricher(match, _options) { + const roll = rollCommandToJSON(match[1], match[0]); + if (!roll) return match[0]; + + return getFateMessage(roll.result, roll.flavor ?? 'FLAVOR'); +} + +function getFateMessage(roll, flavor) { + const label = flavor ?? 'fate'; + + const dataLabel = game.i18n.localize('DAGGERHEART.GENERAL.fate'); + + const fateElement = document.createElement('span'); + fateElement.innerHTML = ` + - - - {{#unless (eq @root.rollType 'D20Roll')}} + {{/each}} + +
+ {{localize "DAGGERHEART.GENERAL.Modifier.plural"}}
- - + +
- {{#if abilities}} - {{localize "DAGGERHEART.GENERAL.traitModifier"}} - + {{#times 10}} + + {{/times}} + + + + {{#if abilities}} + {{localize "DAGGERHEART.GENERAL.traitModifier"}} + + {{/if}} + {{/unless}} + {{#if @root.rallyDie.length}} + {{localize "DAGGERHEART.CLASS.Feature.rallyDice"}} + {{/if}} - {{/unless}} - {{#if @root.rallyDie.length}} - {{localize "DAGGERHEART.CLASS.Feature.rallyDice"}} - - {{/if}} - {{#if (eq @root.rollType 'DualityRoll')}}{{localize "DAGGERHEART.GENERAL.situationalBonus"}}{{/if}} - -
+ {{#if (eq @root.rollType 'DualityRoll')}}{{localize "DAGGERHEART.GENERAL.situationalBonus"}}{{/if}} + + + {{/if}} {{/unless}} {{#if (or costs uses)}} From adfb0e721341ddd397c987d9ebc53bb5eb3e0a6d Mon Sep 17 00:00:00 2001 From: Chris Ryan Date: Thu, 27 Nov 2025 23:31:55 +1000 Subject: [PATCH 05/30] Hide formula display; code removal; start to add Fear die as a choice for Fate roll --- module/data/chat-message/actorRoll.mjs | 2 +- module/dice/fateRoll.mjs | 57 ++----------------- module/enrichers/FateRollEnricher.mjs | 7 ++- templates/dialogs/dice-roll/rollSelection.hbs | 20 ++++++- 4 files changed, 29 insertions(+), 57 deletions(-) diff --git a/module/data/chat-message/actorRoll.mjs b/module/data/chat-message/actorRoll.mjs index 61262529..340d7a9e 100644 --- a/module/data/chat-message/actorRoll.mjs +++ b/module/data/chat-message/actorRoll.mjs @@ -50,7 +50,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { }), damage: new fields.ObjectField(), costs: new fields.ArrayField(new fields.ObjectField()), - successConsumed: new fields.BooleanField({ initial: false }) + successConsumed: new fields.BooleanField({ initial: false }), }; } diff --git a/module/dice/fateRoll.mjs b/module/dice/fateRoll.mjs index 616a11db..783691a8 100644 --- a/module/dice/fateRoll.mjs +++ b/module/dice/fateRoll.mjs @@ -34,6 +34,11 @@ export default class FateRoll extends D20Roll { return false; } + + get fateDie() { + return "Hope"; + } + static getHooks(hooks) { return [...(hooks ?? []), 'Fate']; } @@ -67,61 +72,9 @@ export default class FateRoll extends D20Roll { data.hope = { dice: roll.dHope.denomination, value: roll.dHope.total, - rerolled: { - any: roll.dHope.results.some(x => x.rerolled), - rerolls: roll.dHope.results.filter(x => x.rerolled) - } }; return data; } - // static async reroll(rollString, target, message) { - // let parsedRoll = game.system.api.dice.DualityRoll.fromData({ ...rollString, evaluated: false }); - // const term = parsedRoll.terms[target.dataset.dieIndex]; - // await term.reroll(`/r1=${term.total}`); - // if (game.modules.get('dice-so-nice')?.active) { - // const diceSoNiceRoll = { - // _evaluated: true, - // dice: [ - // new foundry.dice.terms.Die({ - // ...term, - // faces: term._faces, - // results: term.results.filter(x => !x.rerolled) - // }) - // ], - // options: { appearance: {} } - // }; - - // const diceSoNicePresets = await getDiceSoNicePresets(`d${term._faces}`, `d${term._faces}`); - // const type = target.dataset.type; - // if (diceSoNicePresets[type]) { - // diceSoNiceRoll.dice[0].options = diceSoNicePresets[type]; - // } - - // await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true); - // } - - // await parsedRoll.evaluate(); - - // const newRoll = game.system.api.dice.DualityRoll.postEvaluate(parsedRoll, { - // targets: message.system.targets, - // roll: { - // advantage: message.system.roll.advantage?.type, - // difficulty: message.system.roll.difficulty ? Number(message.system.roll.difficulty) : null - // } - // }); - // newRoll.extra = newRoll.extra.slice(2); - - // const tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); - // Hooks.call(`${CONFIG.DH.id}.postRollDuality`, { - // source: { actor: message.system.source.actor ?? '' }, - // targets: message.system.targets, - // tagTeamSelected: Object.values(tagTeamSettings.members).some(x => x.messageId === message._id), - // roll: newRoll, - // rerolledRoll: - // newRoll.result.duality !== message.system.roll.result.duality ? message.system.roll : undefined - // }); - // return { newRoll, parsedRoll }; - // } } diff --git a/module/enrichers/FateRollEnricher.mjs b/module/enrichers/FateRollEnricher.mjs index 0fb4f335..997315eb 100644 --- a/module/enrichers/FateRollEnricher.mjs +++ b/module/enrichers/FateRollEnricher.mjs @@ -1,15 +1,15 @@ -import { abilities } from '../config/actorConfig.mjs'; import { getCommandTarget, rollCommandToJSON } from '../helpers/utils.mjs'; export default function DhFateRollEnricher(match, _options) { const roll = rollCommandToJSON(match[1], match[0]); if (!roll) return match[0]; - return getFateMessage(roll.result, roll.flavor ?? 'FLAVOR'); + return getFateMessage(roll.result, roll?.flavor); } function getFateMessage(roll, flavor) { - const label = flavor ?? 'fate'; + const label = flavor ?? 'Fate'; + console.log("ROLL", roll); const dataLabel = game.i18n.localize('DAGGERHEART.GENERAL.fate'); @@ -19,6 +19,7 @@ function getFateMessage(roll, flavor) { data-title="${label}" data-label="${dataLabel}" data-hope="${roll?.hope ?? 'd12'}" + data-fear="${roll?.fear ?? 'd12'}" ${label} `; diff --git a/templates/dialogs/dice-roll/rollSelection.hbs b/templates/dialogs/dice-roll/rollSelection.hbs index d59c330f..4f0c99c7 100644 --- a/templates/dialogs/dice-roll/rollSelection.hbs +++ b/templates/dialogs/dice-roll/rollSelection.hbs @@ -69,6 +69,8 @@ {{/if}} {{/if}} {{#if (eq @root.rollType 'FateRoll')}} + {{#if (eq @root.roll.fateDie 'Hope')}} +
@@ -78,6 +80,20 @@
+ {{/if}} + + {{#if (eq @root.roll.fateDie 'Fear')}} +
+ +
+ {{localize "DAGGERHEART.GENERAL.fear"}} + +
+
+ {{/if}} + {{/if}} @@ -154,7 +170,9 @@ {{> 'systems/daggerheart/templates/dialogs/dice-roll/costSelection.hbs'}} {{/if}} - {{localize "DAGGERHEART.GENERAL.formula"}}: {{@root.formula}} + {{#if (ne @root.rollType 'FateRoll')}} + {{localize "DAGGERHEART.GENERAL.formula"}}: {{@root.formula}} + {{/if}}
{{/if}} {{/unless}} + {{/if}} {{#if @root.rallyDie.length}} {{localize "DAGGERHEART.CLASS.Feature.rallyDice"}}