// TODO: Rename to something sane. import { DS4 } from "../config"; /** * Provides default values for all arguments the `CheckFactory` expects. */ class DefaultCheckOptions implements DS4CheckFactoryOptions { maxCritSuccess = 1; minCritFailure = 20; useSlayingDice = false; rollMode: DS4RollMode = "roll"; mergeWith(other: Partial): DS4CheckFactoryOptions { return { ...this, ...other } as DS4CheckFactoryOptions; } } /** * 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 checkTargetValue: number, private gmModifier: number, passedOptions: Partial = {}, ) { this.checkOptions = new DefaultCheckOptions().mergeWith(passedOptions); } private checkOptions: DS4CheckFactoryOptions; async execute(): Promise { const rollCls: typeof Roll = CONFIG.Dice.rolls[0]; const formula = [ "ds", this.createTargetValueTerm(), this.createCritTerm(), this.createSlayingDiceTerm(), ].filterJoin(""); const roll = new rollCls(formula); const rollModeTemplate = this.checkOptions.rollMode; console.log(rollModeTemplate); return roll.toMessage({}, { rollMode: rollModeTemplate, create: true }); } // Term generators createTargetValueTerm(): string { if (this.checkTargetValue != null) { return "v" + (this.checkTargetValue + this.gmModifier); } else { return null; } } createCritTerm(): string { const minCritRequired = this.checkOptions.minCritFailure !== defaultCheckOptions.minCritFailure; const maxCritRequired = this.checkOptions.maxCritSuccess !== defaultCheckOptions.maxCritSuccess; if (minCritRequired || maxCritRequired) { return "c" + (this.checkOptions.maxCritSuccess ?? "") + "," + (this.checkOptions.minCritFailure ?? ""); } else { return null; } } createSlayingDiceTerm(): string { return this.checkOptions.useSlayingDice ? "x" : null; } } /** * Asks the user for all unknown/necessary information and passes them on to perform a roll. * @param targetValue {number} The Check Target Number ("CTN") * @param options {Partial} Options changing the behaviour of the roll and message. */ export async function createCheckRoll(targetValue: number, options: Partial): Promise { // Ask for additional required data; const gmModifierData = await askGmModifier(targetValue, options); const newOptions: Partial = { maxCritSuccess: gmModifierData.maxCritSuccess ?? options.maxCritSuccess ?? undefined, minCritFailure: gmModifierData.minCritFailure ?? options.minCritFailure ?? undefined, useSlayingDice: gmModifierData.useSlayingDice ?? options.useSlayingDice ?? undefined, rollMode: gmModifierData.rollMode ?? options.rollMode ?? undefined, }; // Create Factory const cf = new CheckFactory(gmModifierData.checkTargetValue, gmModifierData.gmModifier, newOptions); // Possibly additional processing // Execute roll await 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 {Promise} The data given by the user. */ async function askGmModifier( targetValue: number, options: Partial, { template, title }: { template?: string; title?: string } = {}, ): Promise { // Render model interface and return value const usedTemplate = template ?? "systems/ds4/templates/roll/roll-options.hbs"; const usedTitle = title ?? game.i18n.localize("DS4.RollDialogDefaultTitle"); const templateData = { cssClass: "roll-option", title: usedTitle, checkTargetValue: targetValue, maxCritSuccess: options.maxCritSuccess ?? defaultCheckOptions.maxCritSuccess, minCritFailure: options.minCritFailure ?? defaultCheckOptions.minCritFailure, rollModes: rollModes, config: DS4, }; const renderedHtml = await renderTemplate(usedTemplate, templateData); const dialogPromise = new Promise((resolve) => { new Dialog( { title: usedTitle, close: () => { // Don't do anything }, content: renderedHtml, buttons: { ok: { label: game.i18n.localize("DS4.RollDialogOkButton"), callback: (html: HTMLElement | JQuery) => { if (!("jquery" in html)) { throw new Error( game.i18n.format("DS4.HtmlTypeError", { exType: "JQuery", realType: "HTMLElement", }), ); } else { const innerForm = html[0].querySelector("form"); resolve(innerForm); } }, }, cancel: { label: game.i18n.localize("DS4.RollDialogCancelButton"), callback: () => { // Don't do anything }, }, }, default: "ok", }, {}, ).render(true); }); const dialogForm = await dialogPromise; return parseDialogFormData(dialogForm, targetValue); } /** * Extracts Dialog data from the returned DOM element. * @param formData {HTMLFormElement} The filed dialog * @param targetValue {number} The previously known target value (slated for removal once data automation is available) */ function parseDialogFormData(formData: HTMLFormElement, targetValue: number): IntermediateGmModifierData { return { checkTargetValue: parseInt(formData["ctv"]?.value) ?? targetValue, gmModifier: parseInt(formData["gmmod"]?.value) ?? 0, maxCritSuccess: parseInt(formData["maxcoup"]?.value) ?? defaultCheckOptions.maxCritSuccess, minCritFailure: parseInt(formData["minfumble"]?.value) ?? defaultCheckOptions.minCritFailure, useSlayingDice: false, rollMode: formData["visibility"]?.value ?? defaultCheckOptions.rollMode, }; } /** * Contains data that needs retrieval from an interactive Dialog. */ interface GmModifierData { gmModifier: number; rollMode: DS4RollMode; } /** * Contains *CURRENTLY* necessary Data for drafting a roll. * * @deprecated * Quite a lot of this information is requested due to a lack of automation: * - maxCritSuccess * - minCritFailure * - useSlayingDice * - checkTargetValue * * They will and should be removed once effects and data retrieval is in place. * If a "raw" roll dialog is necessary, create another pre-porcessing Dialog * class asking for the required information. * This interface should then be replaced with the `GmModifierData`. */ interface IntermediateGmModifierData extends GmModifierData { checkTargetValue: number; gmModifier: number; maxCritSuccess: number; minCritFailure: number; // TODO: In final version from system settings useSlayingDice: boolean; rollMode: DS4RollMode; } /** * The minimum behavioural options that need to be passed to the factory. */ export interface DS4CheckFactoryOptions { maxCritSuccess: number; minCritFailure: number; useSlayingDice: boolean; rollMode: DS4RollMode; } /** * Defines all possible roll modes, both for iterating and typing. */ const rollModes = ["roll", "gmroll", "blindroll", "selfroll"] as const; type DS4RollModeTuple = typeof rollModes; export type DS4RollMode = DS4RollModeTuple[number];