The fix has 2 components: 1. The logic for evaluating checks now supports check target numbers<= 0 by still using a single die in this case 2. The CheckFactory sets the check target number to 0 even if it would be < 0. This is necessary because negative numbers would interfer with foundry's math evaluation in rolls and would not be picked up correctly.
212 lines
7.7 KiB
TypeScript
212 lines
7.7 KiB
TypeScript
/**
|
|
* 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>): 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<DS4CheckFactoryOptions> = {},
|
|
) {
|
|
this.options = defaultCheckOptions.mergeWith(options);
|
|
}
|
|
|
|
private options: DS4CheckFactoryOptions;
|
|
|
|
async execute(): Promise<ChatMessage> {
|
|
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 {
|
|
return "v" + Math.max(this.checkTargetNumber + this.gmModifier, 0);
|
|
}
|
|
|
|
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<DS4CheckFactoryOptions> = {},
|
|
): Promise<ChatMessage | unknown> {
|
|
// Ask for additional required data;
|
|
const gmModifierData = await askGmModifier(checkTargetNumber, options);
|
|
|
|
const newTargetValue = gmModifierData.checkTargetNumber ?? checkTargetNumber;
|
|
const gmModifier = gmModifierData.gmModifier ?? 0;
|
|
const newOptions: Partial<DS4CheckFactoryOptions> = {
|
|
maximumCoupResult: gmModifierData.maximumCoupResult ?? options.maximumCoupResult,
|
|
minimumFumbleResult: gmModifierData.minimumFumbleResult ?? options.minimumFumbleResult,
|
|
useSlayingDice: game.settings.get("ds4", "useSlayingDiceForAutomatedChecks"),
|
|
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<DS4CheckFactoryOptions> = {},
|
|
{ template, title }: { template?: string; title?: string } = {},
|
|
): Promise<Partial<IntermediateGmModifierData>> {
|
|
const usedTemplate = template ?? "systems/ds4/templates/dialogs/roll-options.hbs";
|
|
const usedTitle = title ?? game.i18n.localize("DS4.DialogRollOptionsDefaultTitle");
|
|
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<HTMLFormElement>((resolve) => {
|
|
new Dialog({
|
|
title: usedTitle,
|
|
content: renderedHtml,
|
|
buttons: {
|
|
ok: {
|
|
icon: '<i class="fas fa-check"></i>',
|
|
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: '<i class="fas fa-times"></i>',
|
|
label: game.i18n.localize("DS4.GenericCancelButton"),
|
|
},
|
|
},
|
|
default: "ok",
|
|
}).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<IntermediateGmModifierData> {
|
|
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;
|
|
}
|