/** * Provides default values for all arguments the `CheckFactory` expects. */ class DefaultCheckOptions implements DS4CheckFactoryOptions { readonly maximumCoupResult = 1; readonly minimumFumbleResult = 20; readonly useSlayingDice = false; readonly rollMode: Const.DiceRollMode = "roll"; readonly flavor: undefined; mergeWith(other: Partial): DS4CheckFactoryOptions { return { ...this, ...other }; } } /** * Singleton reference for default value extraction. */ const defaultCheckOptions = new DefaultCheckOptions(); /** * Most basic class responsible for generating the chat formula and passing it to the chat as roll. */ class CheckFactory { constructor( private checkTargetNumber: number, private gmModifier: number, options: Partial = {}, ) { this.options = defaultCheckOptions.mergeWith(options); } private options: DS4CheckFactoryOptions; async execute(): Promise { const innerFormula = ["ds", this.createCheckTargetNumberModifier(), this.createCoupFumbleModifier()].filterJoin( "", ); const formula = this.options.useSlayingDice ? `{${innerFormula}}x` : innerFormula; const roll = Roll.create(formula); return roll.toMessage( { speaker: ChatMessage.getSpeaker(), flavor: this.options.flavor }, { rollMode: this.options.rollMode, create: true }, ); } createCheckTargetNumberModifier(): string | null { return "v" + (this.checkTargetNumber + this.gmModifier); } createCoupFumbleModifier(): string | null { const isMinimumFumbleResultRequired = this.options.minimumFumbleResult !== defaultCheckOptions.minimumFumbleResult; const isMaximumCoupResultRequired = this.options.maximumCoupResult !== defaultCheckOptions.maximumCoupResult; if (isMinimumFumbleResultRequired || isMaximumCoupResultRequired) { return "c" + (this.options.maximumCoupResult ?? "") + ":" + (this.options.minimumFumbleResult ?? ""); } else { return null; } } } /** * Asks the user for all unknown/necessary information and passes them on to perform a roll. * @param checkTargetNumber - The Check Target Number ("CTN") * @param options - Options changing the behavior of the roll and message. */ export async function createCheckRoll( checkTargetNumber: number, options: Partial = {}, ): Promise { // Ask for additional required data; const gmModifierData = await askGmModifier(checkTargetNumber, options); const newTargetValue = gmModifierData.checkTargetNumber ?? checkTargetNumber; const gmModifier = gmModifierData.gmModifier ?? 0; const newOptions: Partial = { maximumCoupResult: gmModifierData.maximumCoupResult ?? options.maximumCoupResult, minimumFumbleResult: gmModifierData.minimumFumbleResult ?? options.minimumFumbleResult, useSlayingDice: game.settings.get("ds4", "useSlayingDiceForAutomatedChecks") ?? false, rollMode: gmModifierData.rollMode ?? options.rollMode, flavor: options.flavor, }; // Create Factory const cf = new CheckFactory(newTargetValue, gmModifier, newOptions); // Possibly additional processing // Execute roll return cf.execute(); } /** * Responsible for rendering the modal interface asking for the modifier specified by GM and (currently) additional data. * * @notes * At the moment, this asks for more data than it will do after some iterations. * * @returns The data given by the user. */ async function askGmModifier( checkTargetNumber: number, options: Partial = {}, { template, title }: { template?: string; title?: string } = {}, ): Promise> { const usedTemplate = template ?? "systems/ds4/templates/roll/roll-options.hbs"; const usedTitle = title ?? game.i18n.localize("DS4.RollDialogDefaultTitle"); const templateData = { title: usedTitle, checkTargetNumber: checkTargetNumber, maximumCoupResult: options.maximumCoupResult ?? defaultCheckOptions.maximumCoupResult, minimumFumbleResult: options.minimumFumbleResult ?? defaultCheckOptions.minimumFumbleResult, rollMode: options.rollMode ?? game.settings.get("core", "rollMode"), rollModes: CONFIG.Dice.rollModes, }; const renderedHtml = await renderTemplate(usedTemplate, templateData); const dialogPromise = new Promise((resolve) => { new Dialog( { title: usedTitle, content: renderedHtml, buttons: { ok: { icon: '', label: game.i18n.localize("DS4.GenericOkButton"), callback: (html) => { if (!("jquery" in html)) { throw new Error( game.i18n.format("DS4.ErrorUnexpectedHtmlType", { exType: "JQuery", realType: "HTMLElement", }), ); } else { const innerForm = html[0].querySelector("form"); if (!innerForm) { throw new Error( game.i18n.format("DS4.ErrorCouldNotFindHtmlElement", { htmlElement: "form" }), ); } resolve(innerForm); } }, }, cancel: { icon: '', label: game.i18n.localize("DS4.GenericCancelButton"), }, }, default: "ok", }, { jQuery: true }, ).render(true); }); const dialogForm = await dialogPromise; return parseDialogFormData(dialogForm); } /** * Extracts Dialog data from the returned DOM element. * @param formData - The filed dialog */ function parseDialogFormData(formData: HTMLFormElement): Partial { return { checkTargetNumber: parseInt(formData["check-target-number"]?.value), gmModifier: parseInt(formData["gm-modifier"]?.value), maximumCoupResult: parseInt(formData["maximum-coup-result"]?.value), minimumFumbleResult: parseInt(formData["minimum-fumble-result"]?.value), rollMode: formData["roll-mode"]?.value, }; } /** * Contains data that needs retrieval from an interactive Dialog. */ interface GmModifierData { gmModifier: number; rollMode: Const.DiceRollMode; } /** * Contains *CURRENTLY* necessary Data for drafting a roll. * * @deprecated * Quite a lot of this information is requested due to a lack of automation: * - maximumCoupResult * - minimumFumbleResult * - useSlayingDice * - checkTargetNumber * * They will and should be removed once effects and data retrieval is in place. * If a "raw" roll dialog is necessary, create another pre-processing Dialog * class asking for the required information. * This interface should then be replaced with the `GmModifierData`. */ interface IntermediateGmModifierData extends GmModifierData { checkTargetNumber: number; maximumCoupResult: number; minimumFumbleResult: number; } /** * The minimum behavioral options that need to be passed to the factory. */ export interface DS4CheckFactoryOptions { maximumCoupResult: number; minimumFumbleResult: number; useSlayingDice: boolean; rollMode: Const.DiceRollMode; flavor?: string; }