[Feature] 460 - Reaction Rolls (#481)

* Added a toggle in D20RollDialog for ReactionRolls

* DualityRollEnrichment can now use reaction

* Added flavor for DualityRollEnrichment
This commit is contained in:
WBHarry 2025-07-31 03:27:48 +02:00 committed by GitHub
parent 243630878b
commit e168e3e7ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 103 additions and 31 deletions

View file

@ -187,12 +187,15 @@ Hooks.on('renderHandlebarsApplication', (_, element) => {
Hooks.on('chatMessage', (_, message) => { Hooks.on('chatMessage', (_, message) => {
if (message.startsWith('/dr')) { if (message.startsWith('/dr')) {
const rollCommand = rollCommandToJSON(message.replace(/\/dr\s?/, '')); const result = rollCommandToJSON(message.replace(/\/dr\s?/, ''));
if (!rollCommand) { if (!result) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.dualityParsing')); ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.dualityParsing'));
return false; return false;
} }
const { result: rollCommand, flavor } = result;
const reaction = rollCommand.reaction;
const traitValue = rollCommand.trait?.toLowerCase(); const traitValue = rollCommand.trait?.toLowerCase();
const advantage = rollCommand.advantage const advantage = rollCommand.advantage
? CONFIG.DH.ACTIONS.advantageState.advantage.value ? CONFIG.DH.ACTIONS.advantageState.advantage.value
@ -208,7 +211,16 @@ Hooks.on('chatMessage', (_, message) => {
}) })
: game.i18n.localize('DAGGERHEART.GENERAL.duality'); : game.i18n.localize('DAGGERHEART.GENERAL.duality');
enrichedDualityRoll({ traitValue, target, difficulty, title, label: 'test', actionType: null, advantage }); enrichedDualityRoll({
reaction,
traitValue,
target,
difficulty,
title,
label: 'test',
actionType: null,
advantage
});
return false; return false;
} }
}); });

View file

@ -1821,7 +1821,6 @@
"basics": "Basics", "basics": "Basics",
"bonus": "Bonus", "bonus": "Bonus",
"burden": "Burden", "burden": "Burden",
"check": "{check} Check",
"continue": "Continue", "continue": "Continue",
"criticalSuccess": "Critical Success", "criticalSuccess": "Critical Success",
"damage": "Damage", "damage": "Damage",
@ -1866,6 +1865,7 @@
"proficiency": "Proficiency", "proficiency": "Proficiency",
"quantity": "Quantity", "quantity": "Quantity",
"range": "Range", "range": "Range",
"reactionRoll": "Reaction Roll",
"recovery": "Recovery", "recovery": "Recovery",
"reroll": "Reroll", "reroll": "Reroll",
"rerollThing": "Reroll {thing}", "rerollThing": "Reroll {thing}",
@ -1873,6 +1873,7 @@
"roll": "Roll", "roll": "Roll",
"rollAll": "Roll All", "rollAll": "Roll All",
"rollDamage": "Roll Damage", "rollDamage": "Roll Damage",
"rollWith": "{roll} Roll",
"save": "Save", "save": "Save",
"scalable": "Scalable", "scalable": "Scalable",
"situationalBonus": "Situational Bonus", "situationalBonus": "Situational Bonus",

View file

@ -7,6 +7,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.roll = roll; this.roll = roll;
this.config = config; this.config = config;
this.config.experiences = []; this.config.experiences = [];
this.reactionOverride = config.roll.type === 'reaction';
if (config.source?.action) { if (config.source?.action) {
this.item = config.data.parent.items.get(config.source.item) ?? config.data.parent; this.item = config.data.parent.items.get(config.source.item) ?? config.data.parent;
@ -30,6 +31,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
actions: { actions: {
updateIsAdvantage: this.updateIsAdvantage, updateIsAdvantage: this.updateIsAdvantage,
selectExperience: this.selectExperience, selectExperience: this.selectExperience,
toggleReaction: this.toggleReaction,
submitRoll: this.submitRoll submitRoll: this.submitRoll
}, },
form: { form: {
@ -103,6 +105,9 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.isLite = this.config.roll?.lite; context.isLite = this.config.roll?.lite;
context.extraFormula = this.config.extraFormula; context.extraFormula = this.config.extraFormula;
context.formula = this.roll.constructFormula(this.config); context.formula = this.roll.constructFormula(this.config);
context.showReaction = !context.rollConfig.type && context.rollType === 'DualityRoll';
context.reactionOverride = this.reactionOverride;
} }
return context; return context;
} }
@ -141,7 +146,19 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.render(); this.render();
} }
static toggleReaction() {
if (this.config.roll) {
this.reactionOverride = !this.reactionOverride;
this.render();
}
}
static async submitRoll() { static async submitRoll() {
this.config.roll.type = this.reactionOverride
? CONFIG.DH.ITEM.actionTypes.reaction.id
: this.config.roll.type === CONFIG.DH.ITEM.actionTypes.reaction.id
? null
: this.config.roll.type;
await this.close({ submitted: true }); await this.close({ submitted: true });
} }

View file

@ -69,6 +69,7 @@ export default class DHRoll extends Roll {
static postEvaluate(roll, config = {}) { static postEvaluate(roll, config = {}) {
return { return {
type: config.roll.type,
total: roll.total, total: roll.total,
formula: roll.formula, formula: roll.formula,
dice: roll.dice.map(d => ({ dice: roll.dice.map(d => ({

View file

@ -2,22 +2,23 @@ import { abilities } from '../config/actorConfig.mjs';
import { getCommandTarget, rollCommandToJSON } from '../helpers/utils.mjs'; import { getCommandTarget, rollCommandToJSON } from '../helpers/utils.mjs';
export default function DhDualityRollEnricher(match, _options) { export default function DhDualityRollEnricher(match, _options) {
const roll = rollCommandToJSON(match[1]); const roll = rollCommandToJSON(match[1], match[0]);
if (!roll) return match[0]; if (!roll) return match[0];
return getDualityMessage(roll); return getDualityMessage(roll.result, roll.flavor);
} }
function getDualityMessage(roll) { function getDualityMessage(roll, flavor) {
const traitLabel = const trait = roll.trait && abilities[roll.trait] ? game.i18n.localize(abilities[roll.trait].label) : null;
roll.trait && abilities[roll.trait] const label =
? game.i18n.format('DAGGERHEART.GENERAL.check', { flavor ??
check: game.i18n.localize(abilities[roll.trait].label) (roll.trait
}) ? game.i18n.format('DAGGERHEART.GENERAL.rollWith', { roll: trait })
: null; : roll.reaction
? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll')
: game.i18n.localize('DAGGERHEART.GENERAL.duality'));
const label = traitLabel ?? game.i18n.localize('DAGGERHEART.GENERAL.duality'); const dataLabel = trait
const dataLabel = traitLabel
? game.i18n.localize(abilities[roll.trait].label) ? game.i18n.localize(abilities[roll.trait].label)
: game.i18n.localize('DAGGERHEART.GENERAL.duality'); : game.i18n.localize('DAGGERHEART.GENERAL.duality');
@ -38,6 +39,7 @@ function getDualityMessage(roll) {
<button class="duality-roll-button" <button class="duality-roll-button"
data-title="${label}" data-title="${label}"
data-label="${dataLabel}" data-label="${dataLabel}"
data-reaction="${roll.reaction ? 'true' : 'false'}"
data-hope="${roll.hope ?? 'd12'}" data-hope="${roll.hope ?? 'd12'}"
data-fear="${roll.fear ?? 'd12'}" data-fear="${roll.fear ?? 'd12'}"
${advantage ? `data-advantage="${advantage}"` : ''} ${advantage ? `data-advantage="${advantage}"` : ''}
@ -46,9 +48,9 @@ function getDualityMessage(roll) {
${roll.advantage ? 'data-advantage="true"' : ''} ${roll.advantage ? 'data-advantage="true"' : ''}
${roll.disadvantage ? 'data-disadvantage="true"' : ''} ${roll.disadvantage ? 'data-disadvantage="true"' : ''}
> >
<i class="fa-solid fa-circle-half-stroke"></i> ${roll.reaction ? '<i class="fa-solid fa-reply"></i>' : '<i class="fa-solid fa-circle-half-stroke"></i>'}
${label} ${label}
${roll.difficulty || advantageLabel ? `(${[roll.difficulty, advantageLabel ? game.i18n.localize(`DAGGERHEART.GENERAL.${advantageLabel}.short`) : null].filter(x => x).join(' ')})` : ''} ${!flavor && (roll.difficulty || advantageLabel) ? `(${[roll.difficulty, advantageLabel ? game.i18n.localize(`DAGGERHEART.GENERAL.${advantageLabel}.short`) : null].filter(x => x).join(' ')})` : ''}
</button> </button>
`; `;
@ -57,6 +59,7 @@ function getDualityMessage(roll) {
export const renderDualityButton = async event => { export const renderDualityButton = async event => {
const button = event.currentTarget, const button = event.currentTarget,
reaction = button.dataset.reaction === 'true',
traitValue = button.dataset.trait?.toLowerCase(), traitValue = button.dataset.trait?.toLowerCase(),
target = getCommandTarget({ allowNull: true }), target = getCommandTarget({ allowNull: true }),
difficulty = button.dataset.difficulty, difficulty = button.dataset.difficulty,
@ -64,12 +67,12 @@ export const renderDualityButton = async event => {
await enrichedDualityRoll( await enrichedDualityRoll(
{ {
reaction,
traitValue, traitValue,
target, target,
difficulty, difficulty,
title: button.dataset.title, title: button.dataset.title,
label: button.dataset.label, label: button.dataset.label,
actionType: button.dataset.actionType,
advantage advantage
}, },
event event
@ -77,7 +80,7 @@ export const renderDualityButton = async event => {
}; };
export const enrichedDualityRoll = async ( export const enrichedDualityRoll = async (
{ traitValue, target, difficulty, title, label, actionType, advantage }, { reaction, traitValue, target, difficulty, title, label, advantage },
event event
) => { ) => {
const config = { const config = {
@ -88,7 +91,7 @@ export const enrichedDualityRoll = async (
label: label, label: label,
difficulty: difficulty, difficulty: difficulty,
advantage, advantage,
type: actionType ?? null // Need check, type: reaction ? 'reaction' : null
}, },
chatMessage: { chatMessage: {
template: 'systems/daggerheart/templates/ui/chat/duality-roll.hbs' template: 'systems/daggerheart/templates/ui/chat/duality-roll.hbs'

View file

@ -7,19 +7,19 @@ export { DhDamageEnricher, DhDualityRollEnricher, DhEffectEnricher, DhTemplateEn
export const enricherConfig = [ export const enricherConfig = [
{ {
pattern: /^@Damage\[(.*)\]$/g, pattern: /^@Damage\[(.*)\]({.*})?$/g,
enricher: DhDamageEnricher enricher: DhDamageEnricher
}, },
{ {
pattern: /\[\[\/dr\s?(.*?)\]\]/g, pattern: /\[\[\/dr\s?(.*?)\]\]({.*})?/g,
enricher: DhDualityRollEnricher enricher: DhDualityRollEnricher
}, },
{ {
pattern: /^@Effect\[(.*)\]$/g, pattern: /^@Effect\[(.*)\]({.*})?$/g,
enricher: DhEffectEnricher enricher: DhEffectEnricher
}, },
{ {
pattern: /^@Template\[(.*)\]$/g, pattern: /^@Template\[(.*)\]({.*})?$/g,
enricher: DhTemplateEnricher enricher: DhTemplateEnricher
} }
]; ];

View file

@ -5,9 +5,12 @@ export const capitalize = string => {
return string.charAt(0).toUpperCase() + string.slice(1); return string.charAt(0).toUpperCase() + string.slice(1);
}; };
export function rollCommandToJSON(text) { export function rollCommandToJSON(text, raw) {
if (!text) return {}; if (!text) return {};
const flavorMatch = raw?.match(/{(.*)}$/);
const flavor = flavorMatch ? flavorMatch[1] : null;
// Match key="quoted string" OR key=unquotedValue // Match key="quoted string" OR key=unquotedValue
const PAIR_RE = /(\w+)=("(?:[^"\\]|\\.)*"|\S+)/g; const PAIR_RE = /(\w+)=("(?:[^"\\]|\\.)*"|\S+)/g;
const result = {}; const result = {};
@ -28,7 +31,7 @@ export function rollCommandToJSON(text) {
} }
result[key] = value; result[key] = value;
} }
return Object.keys(result).length > 0 ? result : null; return Object.keys(result).length > 0 ? { result, flavor } : null;
} }
export const getCommandTarget = (options = {}) => { export const getCommandTarget = (options = {}) => {

View file

@ -9,6 +9,36 @@
} }
.application.daggerheart.dialog.dh-style.views.roll-selection { .application.daggerheart.dialog.dh-style.views.roll-selection {
.dialog-header {
display: flex;
justify-content: center;
h1 {
width: auto;
display: flex;
align-items: center;
gap: 8px;
.reaction-roll-controller {
width: auto;
opacity: 0.3;
border-radius: 50%;
font-size: 18px;
font-weight: bold;
&:hover {
opacity: 0.5;
background: light-dark(transparent, @golden);
color: light-dark(@dark-blue, @dark-blue);
}
&.active {
opacity: 1;
}
}
}
}
.roll-dialog-container { .roll-dialog-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -16,6 +46,7 @@
max-width: 550px; max-width: 550px;
.dices-section { .dices-section {
position: relative;
display: flex; display: flex;
gap: 60px; gap: 60px;
justify-content: center; justify-content: center;

View file

@ -1,7 +1,10 @@
<header class="dialog-header"> <header class="dialog-header">
{{#if rollConfig.headerTitle}} <h1>
<h1>{{rollConfig.headerTitle}}</h1> {{ifThen rollConfig.headerTitle rollConfig.headerTitle rollConfig.title}}
{{else}} {{#if showReaction}}
<h1>{{rollConfig.title}}</h1> <button class="reaction-roll-controller {{#if reactionOverride}}active{{/if}}" data-action="toggleReaction" data-tooltip-text="{{localize "DAGGERHEART.GENERAL.reactionRoll"}}">
{{/if}} <i class="fa-solid fa-reply"></i>
</button>
{{/if}}
</h1>
</header> </header>

View file

@ -40,6 +40,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="dice-option"> <div class="dice-option">
<img class="dice-icon" src="{{concat 'systems/daggerheart/assets/icons/dice/fear/' @root.roll.dFear.denomination '.svg'}}" alt=""> <img class="dice-icon" src="{{concat 'systems/daggerheart/assets/icons/dice/fear/' @root.roll.dFear.denomination '.svg'}}" alt="">
<div class="dice-select"> <div class="dice-select">