From 54996e7e12067d20bd7849c235528e4e90a82331 Mon Sep 17 00:00:00 2001 From: Chris Ryan Date: Wed, 26 Nov 2025 23:06:53 +1000 Subject: [PATCH] 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 = ` +