ds4/src/dice/check-factory.ts

278 lines
11 KiB
TypeScript
Raw Normal View History

2021-06-26 22:02:00 +02:00
// SPDX-FileCopyrightText: 2021 Johannes Loher
// SPDX-FileCopyrightText: 2021 Oliver Rümpelein
//
// SPDX-License-Identifier: MIT
2022-05-13 19:12:28 +02:00
import { DialogWithListeners } from "../apps/dialog-with-listeners";
import { DS4 } from "../config";
2022-11-04 21:47:18 +01:00
import { getGame } from "../utils/utils";
2021-07-07 19:22:35 +02:00
2021-01-09 23:21:57 +01:00
/**
* Provides default values for all arguments the `CheckFactory` expects.
*/
class DefaultCheckOptions implements DS4CheckFactoryOptions {
readonly maximumCoupResult = 1;
readonly minimumFumbleResult = 20;
2021-01-25 01:25:45 +01:00
readonly useSlayingDice = false;
readonly rollMode: keyof CONFIG.Dice.RollModes = "publicroll";
2021-03-18 08:52:02 +01:00
readonly flavor: undefined;
2021-01-09 23:21:57 +01:00
mergeWith(other: Partial<DS4CheckFactoryOptions>): DS4CheckFactoryOptions {
2021-02-05 02:52:55 +01:00
return { ...this, ...other };
}
}
2021-01-13 18:56:19 +01:00
/**
* Singleton reference for default value extraction.
*/
const defaultCheckOptions = new DefaultCheckOptions();
2021-01-09 23:21:57 +01:00
/**
* Most basic class responsible for generating the chat formula and passing it to the chat as roll.
*/
class CheckFactory {
constructor(
2021-03-18 08:52:02 +01:00
private checkTargetNumber: number,
2022-05-13 19:12:28 +02:00
private checkModifier: number,
2021-03-18 08:52:02 +01:00
options: Partial<DS4CheckFactoryOptions> = {},
) {
2021-03-18 08:52:02 +01:00
this.options = defaultCheckOptions.mergeWith(options);
}
2021-01-13 17:11:07 +01:00
2021-03-18 08:52:02 +01:00
private options: DS4CheckFactoryOptions;
2021-06-30 04:32:10 +02:00
async execute(): Promise<ChatMessage | undefined> {
2021-03-18 08:58:35 +01:00
const innerFormula = ["ds", this.createCheckTargetNumberModifier(), this.createCoupFumbleModifier()].filterJoin(
"",
);
2021-03-18 08:52:02 +01:00
const formula = this.options.useSlayingDice ? `{${innerFormula}}x` : innerFormula;
const roll = Roll.create(formula);
const speaker = this.options.speaker ?? ChatMessage.getSpeaker();
2021-06-30 04:32:10 +02:00
2021-07-01 02:56:09 +02:00
return roll.toMessage(
{
speaker,
flavor: this.options.flavor,
flags: this.options.flavorData ? { ds4: { flavorData: this.options.flavorData } } : undefined,
},
2021-03-18 08:52:02 +01:00
{ rollMode: this.options.rollMode, create: true },
);
}
createCheckTargetNumberModifier(): string {
const totalCheckTargetNumber = this.checkTargetNumber + this.checkModifier;
return totalCheckTargetNumber >= 0 ? `v(${this.checkTargetNumber} + ${this.checkModifier})` : "v0";
}
2021-03-18 08:52:02 +01:00
createCoupFumbleModifier(): string | null {
const isMinimumFumbleResultRequired =
this.options.minimumFumbleResult !== defaultCheckOptions.minimumFumbleResult;
const isMaximumCoupResultRequired = this.options.maximumCoupResult !== defaultCheckOptions.maximumCoupResult;
2021-03-18 08:52:02 +01:00
if (isMinimumFumbleResultRequired || isMaximumCoupResultRequired) {
return `c${this.options.maximumCoupResult ?? ""}:${this.options.minimumFumbleResult ?? ""}`;
} else {
return null;
}
}
}
2021-01-09 23:21:57 +01:00
/**
* Asks the user for all unknown/necessary information and passes them on to perform a roll.
2021-03-18 08:52:02 +01:00
* @param checkTargetNumber - The Check Target Number ("CTN")
* @param options - Options changing the behavior of the roll and message.
2021-01-09 23:21:57 +01:00
*/
export async function createCheckRoll(
2021-03-18 08:52:02 +01:00
checkTargetNumber: number,
options: Partial<DS4CheckFactoryOptions> = {},
): Promise<ChatMessage | unknown> {
// Ask for additional required data;
2022-05-13 19:12:28 +02:00
const interactiveRollData = await askForInteractiveRollData(checkTargetNumber, options);
2022-05-13 19:12:28 +02:00
const newTargetValue = interactiveRollData.checkTargetNumber ?? checkTargetNumber;
const checkModifier = interactiveRollData.checkModifier ?? 0;
const newOptions: Partial<DS4CheckFactoryOptions> = {
2022-05-13 19:12:28 +02:00
maximumCoupResult: interactiveRollData.maximumCoupResult ?? options.maximumCoupResult,
minimumFumbleResult: interactiveRollData.minimumFumbleResult ?? options.minimumFumbleResult,
2021-07-07 19:22:35 +02:00
useSlayingDice: getGame().settings.get("ds4", "useSlayingDiceForAutomatedChecks"),
2022-05-13 19:12:28 +02:00
rollMode: interactiveRollData.rollMode ?? options.rollMode,
2021-03-18 08:52:02 +01:00
flavor: options.flavor,
flavorData: options.flavorData,
speaker: options.speaker,
};
// Create Factory
2022-05-13 19:12:28 +02:00
const cf = new CheckFactory(newTargetValue, checkModifier, newOptions);
// Possibly additional processing
// Execute roll
return cf.execute();
}
2021-01-09 23:21:57 +01:00
/**
2022-05-13 19:12:28 +02:00
* Responsible for rendering the modal interface asking for the check modifier and (currently) additional
* data.
2021-01-09 23:21:57 +01:00
*
* @notes
* At the moment, this asks for more data than it will do after some iterations.
*
* @returns The data given by the user.
2021-01-09 23:21:57 +01:00
*/
2022-05-13 19:12:28 +02:00
async function askForInteractiveRollData(
checkTargetNumber: number,
options: Partial<DS4CheckFactoryOptions> = {},
{ template, title }: { template?: string; title?: string } = {},
2022-05-13 19:12:28 +02:00
): Promise<Partial<IntermediateInteractiveRollData>> {
2021-04-13 21:40:52 +02:00
const usedTemplate = template ?? "systems/ds4/templates/dialogs/roll-options.hbs";
2021-07-07 19:22:35 +02:00
const usedTitle = title ?? getGame().i18n.localize("DS4.DialogRollOptionsDefaultTitle");
const id = foundry.utils.randomID();
const templateData = {
2021-01-13 18:02:22 +01:00
title: usedTitle,
checkTargetNumber: checkTargetNumber,
maximumCoupResult: options.maximumCoupResult ?? defaultCheckOptions.maximumCoupResult,
minimumFumbleResult: options.minimumFumbleResult ?? defaultCheckOptions.minimumFumbleResult,
2021-07-07 19:22:35 +02:00
rollMode: options.rollMode ?? getGame().settings.get("core", "rollMode"),
rollModes: CONFIG.Dice.rollModes,
2022-05-13 19:12:28 +02:00
checkModifiers: Object.entries(DS4.i18n.checkModifiers).map(([key, translation]) => {
if (key in DS4.checkModifiers) {
const value = DS4.checkModifiers[key as keyof typeof DS4.checkModifiers];
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<HTMLFormElement>((resolve) => {
2022-05-13 19:12:28 +02:00
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)) {
2021-02-07 13:51:20 +01:00
throw new Error(
2022-05-13 19:12:28 +02:00
getGame().i18n.format("DS4.ErrorUnexpectedHtmlType", {
exType: "JQuery",
realType: "HTMLElement",
}),
2021-02-07 13:51:20 +01:00
);
2022-05-13 19:12:28 +02:00
} else {
const innerForm = html[0]?.querySelector("form");
if (!innerForm) {
throw new Error(
getGame().i18n.format("DS4.ErrorCouldNotFindHtmlElement", {
htmlElement: "form",
}),
);
}
resolve(innerForm);
2021-02-07 13:51:20 +01:00
}
2022-05-13 19:12:28 +02:00
},
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: getGame().i18n.localize("DS4.GenericCancelButton"),
},
},
2022-05-13 19:12:28 +02:00
default: "ok",
},
{
activateAdditionalListeners: (html, app) => {
const checkModifierCustomFormGroup = html
.find(`#check-modifier-custom-${id}`)
.parent(".form-group");
html.find(`#check-modifier-${id}`).on("change", (event) => {
2022-05-13 19:12:28 +02:00
if (
(event.currentTarget as HTMLSelectElement).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,
},
2022-05-13 19:12:28 +02:00
).render(true);
});
const dialogForm = await dialogPromise;
return parseDialogFormData(dialogForm);
}
2021-01-13 18:56:19 +01:00
/**
* Extracts Dialog data from the returned DOM element.
2021-02-07 11:51:36 +01:00
* @param formData - The filed dialog
2021-01-13 18:56:19 +01:00
*/
2022-05-13 19:12:28 +02:00
function parseDialogFormData(formData: HTMLFormElement): Partial<IntermediateInteractiveRollData> {
const chosenCheckTargetNumber = parseInt(formData["check-target-number"]?.value);
2022-05-13 19:12:28 +02:00
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;
2021-01-13 18:02:22 +01:00
return {
checkTargetNumber: Number.isSafeInteger(chosenCheckTargetNumber) ? chosenCheckTargetNumber : undefined,
2022-05-13 19:12:28 +02:00
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,
};
}
2021-01-13 18:56:19 +01:00
/**
* Contains data that needs retrieval from an interactive Dialog.
*/
2022-05-13 19:12:28 +02:00
interface InteractiveRollData {
checkModifier: number;
rollMode: keyof CONFIG.Dice.RollModes;
2021-01-13 18:56:19 +01:00
}
/**
* 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
2021-01-13 18:56:19 +01:00
*
* 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
2021-01-13 18:56:19 +01:00
* class asking for the required information.
2022-05-13 19:12:28 +02:00
* This interface should then be replaced with the `InteractiveRollData`.
2021-01-13 18:56:19 +01:00
*/
2022-05-13 19:12:28 +02:00
interface IntermediateInteractiveRollData extends InteractiveRollData {
checkTargetNumber: number;
maximumCoupResult: number;
minimumFumbleResult: number;
}
2021-01-13 18:56:19 +01:00
/**
* The minimum behavioral options that need to be passed to the factory.
2021-01-13 18:56:19 +01:00
*/
2021-01-09 23:21:57 +01:00
export interface DS4CheckFactoryOptions {
maximumCoupResult: number;
minimumFumbleResult: number;
useSlayingDice: boolean;
rollMode: keyof CONFIG.Dice.RollModes;
2021-03-18 08:52:02 +01:00
flavor?: string;
flavorData?: Record<string, string | number | null>;
speaker?: ReturnType<typeof ChatMessage.getSpeaker>;
}