ds4/src/module/rolls/check-factory.ts

233 lines
8.2 KiB
TypeScript
Raw Normal View History

2021-01-13 18:02:22 +01:00
import { DS4 } from "../config";
2021-01-09 23:21:57 +01:00
/**
* Provides default values for all arguments the `CheckFactory` expects.
*/
class DefaultCheckOptions implements DS4CheckFactoryOptions {
2021-01-25 01:25:45 +01:00
readonly maxCritSuccess = 1;
readonly minCritFailure = 20;
readonly useSlayingDice = false;
readonly rollMode: DS4RollMode = "roll";
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(
private checkTargetValue: number,
private gmModifier: number,
2021-01-09 23:21:57 +01:00
passedOptions: Partial<DS4CheckFactoryOptions> = {},
) {
this.checkOptions = defaultCheckOptions.mergeWith(passedOptions);
}
2021-01-13 17:11:07 +01:00
2021-01-09 23:21:57 +01:00
private checkOptions: DS4CheckFactoryOptions;
async execute(): Promise<ChatMessage | unknown> {
2021-02-05 02:35:47 +01:00
const rollCls = CONFIG.Dice.rolls[0];
const formula = [
"ds",
this.createTargetValueTerm(),
this.createCritTerm(),
this.createSlayingDiceTerm(),
].filterJoin("");
const roll = new rollCls(formula);
const rollModeTemplate = this.checkOptions.rollMode;
return roll.toMessage({}, { rollMode: rollModeTemplate, create: true });
}
// Term generators
createTargetValueTerm(): string | null {
if (this.checkTargetValue !== null) {
return "v" + (this.checkTargetValue + this.gmModifier);
} else {
return null;
}
}
createCritTerm(): string | null {
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 | null {
return this.checkOptions.useSlayingDice ? "x" : 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.
* @param targetValue The Check Target Number ("CTN")
* @param options Options changing the behaviour of the roll and message.
2021-01-09 23:21:57 +01:00
*/
export async function createCheckRoll(
targetValue: number,
options: Partial<DS4CheckFactoryOptions> = {},
): Promise<ChatMessage | unknown> {
// Ask for additional required data;
const gmModifierData = await askGmModifier(targetValue, options);
const newTargetValue = gmModifierData.checkTargetValue ?? targetValue;
const gmModifier = gmModifierData.gmModifier ?? 0;
const newOptions: Partial<DS4CheckFactoryOptions> = {
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(newTargetValue, gmModifier, newOptions);
// Possibly additional processing
// Execute roll
return cf.execute();
}
2021-01-09 23:21:57 +01:00
/**
2021-01-13 18:56:19 +01:00
* Responsible for rendering the modal interface asking for the modifier specified by GM 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
*/
async function askGmModifier(
targetValue: number,
options: Partial<DS4CheckFactoryOptions> = {},
{ template, title }: { template?: string; title?: string } = {},
): Promise<Partial<IntermediateGmModifierData>> {
// Render model interface and return value
const usedTemplate = template ?? "systems/ds4/templates/roll/roll-options.hbs";
2021-01-13 18:02:22 +01:00
const usedTitle = title ?? game.i18n.localize("DS4.RollDialogDefaultTitle");
const templateData = {
cssClass: "roll-option",
2021-01-13 18:02:22 +01:00
title: usedTitle,
checkTargetValue: targetValue,
maxCritSuccess: options.maxCritSuccess ?? defaultCheckOptions.maxCritSuccess,
minCritFailure: options.minCritFailure ?? defaultCheckOptions.minCritFailure,
rollMode: options.rollMode,
2021-01-13 18:02:22 +01:00
config: DS4,
};
const renderedHtml = await renderTemplate(usedTemplate, templateData);
const dialogPromise = new Promise<HTMLFormElement>((resolve) => {
2021-02-05 02:35:47 +01:00
new Dialog(
{
title: usedTitle,
content: renderedHtml,
buttons: {
ok: {
label: game.i18n.localize("DS4.RollDialogOkButton"),
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");
resolve(innerForm);
}
},
icon: "", // TODO(types): Remove once https://github.com/League-of-Foundry-Developers/foundry-vtt-types/issues/266 is fixed
},
2021-02-05 02:35:47 +01:00
cancel: {
label: game.i18n.localize("DS4.RollDialogCancelButton"),
icon: "", // TODO(types): Remove once https://github.com/League-of-Foundry-Developers/foundry-vtt-types/issues/266 is fixed
},
},
2021-02-05 02:35:47 +01:00
default: "ok",
},
2021-02-05 02:35:47 +01:00
{}, // TODO(types): Remove once https://github.com/League-of-Foundry-Developers/foundry-vtt-types/issues/267 is resolved
).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.
* @param formData The filed dialog
2021-01-13 18:56:19 +01:00
*/
function parseDialogFormData(formData: HTMLFormElement): Partial<IntermediateGmModifierData> {
2021-01-13 18:02:22 +01:00
return {
checkTargetValue: parseInt(formData["ctv"]?.value),
gmModifier: parseInt(formData["gmmod"]?.value),
maxCritSuccess: parseInt(formData["maxcoup"]?.value),
minCritFailure: parseInt(formData["minfumble"]?.value),
rollMode: formData["visibility"]?.value,
};
}
2021-01-13 18:56:19 +01:00
/**
* Contains data that needs retrieval from an interactive Dialog.
*/
interface GmModifierData {
2021-01-13 18:56:19 +01:00
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;
}
2021-01-13 18:56:19 +01:00
/**
* The minimum behavioural options that need to be passed to the factory.
*/
2021-01-09 23:21:57 +01:00
export interface DS4CheckFactoryOptions {
maxCritSuccess: number;
minCritFailure: number;
useSlayingDice: boolean;
2021-01-09 23:21:57 +01:00
rollMode: DS4RollMode;
}
2021-01-13 18:56:19 +01:00
/**
* 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];