307 lines
13 KiB
JavaScript
307 lines
13 KiB
JavaScript
// SPDX-FileCopyrightText: 2021 Johannes Loher
|
|
// SPDX-FileCopyrightText: 2021 Oliver Rümpelein
|
|
//
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
import { DialogWithListeners } from "../apps/dialog-with-listeners";
|
|
import { DS4 } from "../config";
|
|
import { getGame } from "../utils/utils";
|
|
|
|
/** @typedef {"publicroll" | "gmroll" | "gmroll" | "selfroll"} RollModes */
|
|
|
|
/**
|
|
* Most basic class responsible for generating the chat formula and passing it to the chat as roll.
|
|
*/
|
|
class CheckFactory {
|
|
/**
|
|
* @param {number} checkTargetNumber The check target number for this check factory
|
|
* @param {number} checkModifier The check modifier for this check factory
|
|
* @param {Partial<DS4CheckFactoryOptions>} [options] Options for this check factory
|
|
*/
|
|
constructor(checkTargetNumber, checkModifier, options = {}) {
|
|
this.#checkTargetNumber = checkTargetNumber;
|
|
this.#checkModifier = checkModifier;
|
|
this.#options = foundry.utils.mergeObject(this.constructor.defaultOptions, options);
|
|
}
|
|
|
|
/**
|
|
* The check target number for this check factory.
|
|
* @type {number}
|
|
*/
|
|
#checkTargetNumber;
|
|
|
|
/**
|
|
* The check modifier for this check factory.
|
|
* @type {number}
|
|
*/
|
|
#checkModifier;
|
|
|
|
/**
|
|
* The options for this check factory.
|
|
* @type {DS4CheckFactoryOptions}
|
|
*/
|
|
#options;
|
|
|
|
/**
|
|
* The default options of thos CheckFactory class. Upon instantiation, they are merged with the explicitly provided options.
|
|
* @type {DS4CheckFactoryOptions}
|
|
*/
|
|
static get defaultOptions() {
|
|
return {
|
|
maximumCoupResult: 1,
|
|
minimumFumbleResult: 20,
|
|
useSlayingDice: false,
|
|
rollMode: "publicroll",
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Execute this check factory.
|
|
* @returns {Promise<ChatMessage | undefined>} A promise that resolves to the created chat message for the roll */
|
|
async execute() {
|
|
const innerFormula = ["ds", this.createCheckTargetNumberModifier(), this.createCoupFumbleModifier()].filterJoin(
|
|
"",
|
|
);
|
|
const formula = this.#options.useSlayingDice ? `{${innerFormula}}x` : innerFormula;
|
|
const roll = Roll.create(formula);
|
|
const speaker = this.#options.speaker ?? ChatMessage.getSpeaker();
|
|
|
|
return roll.toMessage(
|
|
{
|
|
speaker,
|
|
flavor: this.#options.flavor,
|
|
flags: this.#options.flavorData ? { ds4: { flavorData: this.#options.flavorData } } : undefined,
|
|
},
|
|
{ rollMode: this.#options.rollMode, create: true },
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create the check target number modifier for this roll.
|
|
* @returns string
|
|
*/
|
|
createCheckTargetNumberModifier() {
|
|
const totalCheckTargetNumber = this.#checkTargetNumber + this.#checkModifier;
|
|
return totalCheckTargetNumber >= 0 ? `v${this.#checkTargetNumber + this.#checkModifier}` : "v0";
|
|
}
|
|
|
|
/**
|
|
* Create the coup fumble modifier for this roll.
|
|
* @returns {string | null}
|
|
*/
|
|
createCoupFumbleModifier() {
|
|
const isMinimumFumbleResultRequired =
|
|
this.#options.minimumFumbleResult !== this.constructor.defaultOptions.minimumFumbleResult;
|
|
const isMaximumCoupResultRequired =
|
|
this.#options.maximumCoupResult !== this.constructor.defaultOptions.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 {number} checkTargetNumber The Check Target Number ("CTN")
|
|
* @param {Partial<DS4CheckFactoryOptions>} [options={}] Options changing the behavior of the roll and message.
|
|
* @returns {Promise<ChateMessage|undefined>} A promise that resolves to the chat message created by the roll
|
|
*/
|
|
export async function createCheckRoll(checkTargetNumber, options = {}) {
|
|
// Ask for additional required data;
|
|
const interactiveRollData = await askForInteractiveRollData(checkTargetNumber, options);
|
|
|
|
const newTargetValue = interactiveRollData.checkTargetNumber ?? checkTargetNumber;
|
|
const checkModifier = interactiveRollData.checkModifier ?? 0;
|
|
/** @type {Partial<DS4CheckFactoryOptions>} */
|
|
const newOptions = {
|
|
maximumCoupResult: interactiveRollData.maximumCoupResult ?? options.maximumCoupResult,
|
|
minimumFumbleResult: interactiveRollData.minimumFumbleResult ?? options.minimumFumbleResult,
|
|
useSlayingDice: getGame().settings.get("ds4", "useSlayingDiceForAutomatedChecks"),
|
|
rollMode: interactiveRollData.rollMode ?? options.rollMode,
|
|
flavor: options.flavor,
|
|
flavorData: options.flavorData,
|
|
speaker: options.speaker,
|
|
};
|
|
|
|
// Create Factory
|
|
const cf = new CheckFactory(newTargetValue, checkModifier, newOptions);
|
|
|
|
// Possibly additional processing
|
|
|
|
// Execute roll
|
|
return cf.execute();
|
|
}
|
|
|
|
/**
|
|
* Responsible for rendering the modal interface asking for the check modifier and (currently) additional
|
|
* data.
|
|
*
|
|
* @notes
|
|
* At the moment, this asks for more data than it will do after some iterations.
|
|
*
|
|
* @param {number} checkTargetNumber The check target number
|
|
* @param {Partial<DS4CheckFactoryOptions>} [options={}] Predefined roll options
|
|
* @param {template?: string | undefined; title?: string | undefined} [additionalOptions={}] Additional options to use for the dialog
|
|
* @returns {Promise<Partial<IntermediateInteractiveRollData>>} A promise that resolves to the data given by the user.
|
|
*/
|
|
async function askForInteractiveRollData(checkTargetNumber, options = {}, { template, title } = {}) {
|
|
const usedTemplate = template ?? "systems/ds4/templates/dialogs/roll-options.hbs";
|
|
const usedTitle = title ?? getGame().i18n.localize("DS4.DialogRollOptionsDefaultTitle");
|
|
const id = foundry.utils.randomID();
|
|
const templateData = {
|
|
title: usedTitle,
|
|
checkTargetNumber: checkTargetNumber,
|
|
maximumCoupResult: options.maximumCoupResult ?? this.constructor.defaultOptions.maximumCoupResult,
|
|
minimumFumbleResult: options.minimumFumbleResult ?? this.constructor.defaultOptions.minimumFumbleResult,
|
|
rollMode: options.rollMode ?? getGame().settings.get("core", "rollMode"),
|
|
rollModes: CONFIG.Dice.rollModes,
|
|
checkModifiers: Object.entries(DS4.i18n.checkModifiers).map(([key, translation]) => {
|
|
if (key in DS4.checkModifiers) {
|
|
const value = DS4.checkModifiers[key];
|
|
const label = `${translation} (${value >= 0 ? `+${value}` : value})`;
|
|
return { value, label };
|
|
}
|
|
return { value: key, label: translation };
|
|
}),
|
|
id,
|
|
};
|
|
const renderedHtml = await renderTemplate(usedTemplate, templateData);
|
|
|
|
const dialogPromise = new Promise((resolve) => {
|
|
new DialogWithListeners(
|
|
{
|
|
title: usedTitle,
|
|
content: renderedHtml,
|
|
buttons: {
|
|
ok: {
|
|
icon: '<i class="fas fa-check"></i>',
|
|
label: getGame().i18n.localize("DS4.GenericOkButton"),
|
|
callback: (html) => {
|
|
if (!("jquery" in html)) {
|
|
throw new Error(
|
|
getGame().i18n.format("DS4.ErrorUnexpectedHtmlType", {
|
|
exType: "JQuery",
|
|
realType: "HTMLElement",
|
|
}),
|
|
);
|
|
} else {
|
|
const innerForm = html[0]?.querySelector("form");
|
|
if (!innerForm) {
|
|
throw new Error(
|
|
getGame().i18n.format("DS4.ErrorCouldNotFindHtmlElement", {
|
|
htmlElement: "form",
|
|
}),
|
|
);
|
|
}
|
|
resolve(innerForm);
|
|
}
|
|
},
|
|
},
|
|
cancel: {
|
|
icon: '<i class="fas fa-times"></i>',
|
|
label: getGame().i18n.localize("DS4.GenericCancelButton"),
|
|
},
|
|
},
|
|
default: "ok",
|
|
},
|
|
{
|
|
activateAdditionalListeners: (html, app) => {
|
|
const checkModifierCustomFormGroup = html
|
|
.find(`#check-modifier-custom-${id}`)
|
|
.parent(".form-group");
|
|
html.find(`#check-modifier-${id}`).on("change", (event) => {
|
|
if (
|
|
event.currentTarget.value === "custom" &&
|
|
checkModifierCustomFormGroup.hasClass("ds4-hidden")
|
|
) {
|
|
checkModifierCustomFormGroup.removeClass("ds4-hidden");
|
|
app.setPosition({ height: "auto" });
|
|
} else if (!checkModifierCustomFormGroup.hasClass("ds4-hidden")) {
|
|
checkModifierCustomFormGroup.addClass("ds4-hidden");
|
|
app.setPosition({ height: "auto" });
|
|
}
|
|
});
|
|
},
|
|
id,
|
|
},
|
|
).render(true);
|
|
});
|
|
const dialogForm = await dialogPromise;
|
|
return parseDialogFormData(dialogForm);
|
|
}
|
|
|
|
/**
|
|
* Extracts Dialog data from the returned DOM element.
|
|
* @param {HTMLFormElement} formData The filed dialog
|
|
* @returns {Partial<IntermediateInteractiveRollData>}
|
|
*/
|
|
function parseDialogFormData(formData) {
|
|
const chosenCheckTargetNumber = parseInt(formData["check-target-number"]?.value);
|
|
const chosenCheckModifier = formData["check-modifier"]?.value;
|
|
|
|
const chosenCheckModifierValue =
|
|
chosenCheckModifier === "custom"
|
|
? parseInt(formData["check-modifier-custom"]?.value)
|
|
: parseInt(chosenCheckModifier);
|
|
|
|
const chosenMaximumCoupResult = parseInt(formData["maximum-coup-result"]?.value);
|
|
const chosenMinimumFumbleResult = parseInt(formData["minimum-fumble-result"]?.value);
|
|
const chosenRollMode = formData["roll-mode"]?.value;
|
|
|
|
return {
|
|
checkTargetNumber: Number.isSafeInteger(chosenCheckTargetNumber) ? chosenCheckTargetNumber : undefined,
|
|
checkModifier: Number.isSafeInteger(chosenCheckModifierValue) ? chosenCheckModifierValue : undefined,
|
|
maximumCoupResult: Number.isSafeInteger(chosenMaximumCoupResult) ? chosenMaximumCoupResult : undefined,
|
|
minimumFumbleResult: Number.isSafeInteger(chosenMinimumFumbleResult) ? chosenMinimumFumbleResult : undefined,
|
|
rollMode: Object.keys(CONFIG.Dice.rollModes).includes(chosenRollMode) ? chosenRollMode : undefined,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Contains data that needs retrieval from an interactive Dialog.
|
|
* @typedef {object} InteractiveRollData
|
|
* @property {number} checkModifier
|
|
* @property {RollModes} rollMode
|
|
*/
|
|
|
|
/**
|
|
* 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
|
|
* - 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 {@link InteractiveRollData}.
|
|
* @typedef {object} IntermediateInteractiveRollData
|
|
* @property {number} checkTargetNumber
|
|
* @property {number} maximumCoupResult
|
|
* @property {number} minimumFumbleResult
|
|
*/
|
|
|
|
/**
|
|
* The minimum behavioral options that need to be passed to the factory.
|
|
* @typedef {object} DS4CheckFactoryOptions
|
|
* @property {number} maximumCoupResult
|
|
* @property {number} minimumFumbleResult
|
|
* @property {boolean} useSlayingDice
|
|
* @property {RollModes} rollMode
|
|
* @property {string} [flavor]
|
|
* @property {Record<string, string | number | null>} [flavorData]
|
|
* @property {ChatSpeakerData} [speaker]
|
|
*/
|
|
|
|
/**
|
|
* @typedef {object} ChatSpeakerData
|
|
* @property {string | null} [scene]
|
|
* @property {string | null} [actor]
|
|
* @property {string | null} [token]
|
|
* @property {string | null} [alias]
|
|
*/
|