refactor: convert to ECMAScript where necessary
Also drop @league-of-foundry-developers/foundry-vtt-types.
This commit is contained in:
parent
df4538f6ed
commit
6277e27056
69 changed files with 1077 additions and 1679 deletions
|
@ -99,7 +99,13 @@ function shouldUseCoupForLastSubCheck(
|
|||
);
|
||||
}
|
||||
|
||||
interface SubCheckResult extends DieWithSubCheck, DiceTerm.Result {}
|
||||
interface SubCheckResult extends DieWithSubCheck {
|
||||
active?: boolean;
|
||||
discarded?: boolean;
|
||||
success?: boolean;
|
||||
failure?: boolean;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
function evaluateDiceWithSubChecks(
|
||||
results: DieWithSubCheck[],
|
||||
|
|
|
@ -7,70 +7,96 @@ import { DialogWithListeners } from "../apps/dialog-with-listeners";
|
|||
import { DS4 } from "../config";
|
||||
import { getGame } from "../utils/utils";
|
||||
|
||||
/**
|
||||
* Provides default values for all arguments the `CheckFactory` expects.
|
||||
*/
|
||||
class DefaultCheckOptions implements DS4CheckFactoryOptions {
|
||||
readonly maximumCoupResult = 1;
|
||||
readonly minimumFumbleResult = 20;
|
||||
readonly useSlayingDice = false;
|
||||
readonly rollMode: keyof CONFIG.Dice.RollModes = "publicroll";
|
||||
readonly flavor: undefined;
|
||||
|
||||
mergeWith(other: Partial<DS4CheckFactoryOptions>): DS4CheckFactoryOptions {
|
||||
return { ...this, ...other };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton reference for default value extraction.
|
||||
*/
|
||||
const defaultCheckOptions = new DefaultCheckOptions();
|
||||
/** @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 {
|
||||
constructor(
|
||||
private checkTargetNumber: number,
|
||||
private checkModifier: number,
|
||||
options: Partial<DS4CheckFactoryOptions> = {},
|
||||
) {
|
||||
this.options = defaultCheckOptions.mergeWith(options);
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
|
||||
private options: DS4CheckFactoryOptions;
|
||||
/**
|
||||
* The check target number for this check factory.
|
||||
* @type {number}
|
||||
*/
|
||||
#checkTargetNumber;
|
||||
|
||||
async execute(): Promise<ChatMessage | undefined> {
|
||||
/**
|
||||
* 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 formula = this.#options.useSlayingDice ? `{${innerFormula}}x` : innerFormula;
|
||||
const roll = Roll.create(formula);
|
||||
const speaker = this.options.speaker ?? ChatMessage.getSpeaker();
|
||||
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,
|
||||
flavor: this.#options.flavor,
|
||||
flags: this.#options.flavorData ? { ds4: { flavorData: this.#options.flavorData } } : undefined,
|
||||
},
|
||||
{ rollMode: this.options.rollMode, create: true },
|
||||
{ rollMode: this.#options.rollMode, create: true },
|
||||
);
|
||||
}
|
||||
|
||||
createCheckTargetNumberModifier(): string {
|
||||
const totalCheckTargetNumber = this.checkTargetNumber + this.checkModifier;
|
||||
return totalCheckTargetNumber >= 0 ? `v${this.checkTargetNumber + this.checkModifier}` : "v0";
|
||||
/**
|
||||
* 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";
|
||||
}
|
||||
|
||||
createCoupFumbleModifier(): string | null {
|
||||
/**
|
||||
* Create the coup fumble modifier for this roll.
|
||||
* @returns {string | null}
|
||||
*/
|
||||
createCoupFumbleModifier() {
|
||||
const isMinimumFumbleResultRequired =
|
||||
this.options.minimumFumbleResult !== defaultCheckOptions.minimumFumbleResult;
|
||||
const isMaximumCoupResultRequired = this.options.maximumCoupResult !== defaultCheckOptions.maximumCoupResult;
|
||||
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 ?? ""}`;
|
||||
return `c${this.#options.maximumCoupResult ?? ""}:${this.#options.minimumFumbleResult ?? ""}`;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -79,19 +105,18 @@ class CheckFactory {
|
|||
|
||||
/**
|
||||
* 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.
|
||||
* @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: number,
|
||||
options: Partial<DS4CheckFactoryOptions> = {},
|
||||
): Promise<ChatMessage | unknown> {
|
||||
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;
|
||||
const newOptions: Partial<DS4CheckFactoryOptions> = {
|
||||
/** @type {Partial<DS4CheckFactoryOptions>} */
|
||||
const newOptions = {
|
||||
maximumCoupResult: interactiveRollData.maximumCoupResult ?? options.maximumCoupResult,
|
||||
minimumFumbleResult: interactiveRollData.minimumFumbleResult ?? options.minimumFumbleResult,
|
||||
useSlayingDice: getGame().settings.get("ds4", "useSlayingDiceForAutomatedChecks"),
|
||||
|
@ -117,26 +142,25 @@ export async function createCheckRoll(
|
|||
* @notes
|
||||
* At the moment, this asks for more data than it will do after some iterations.
|
||||
*
|
||||
* @returns The data given by the user.
|
||||
* @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: number,
|
||||
options: Partial<DS4CheckFactoryOptions> = {},
|
||||
{ template, title }: { template?: string; title?: string } = {},
|
||||
): Promise<Partial<IntermediateInteractiveRollData>> {
|
||||
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 ?? defaultCheckOptions.maximumCoupResult,
|
||||
minimumFumbleResult: options.minimumFumbleResult ?? defaultCheckOptions.minimumFumbleResult,
|
||||
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 as keyof typeof DS4.checkModifiers];
|
||||
const value = DS4.checkModifiers[key];
|
||||
const label = `${translation} (${value >= 0 ? `+${value}` : value})`;
|
||||
return { value, label };
|
||||
}
|
||||
|
@ -146,7 +170,7 @@ async function askForInteractiveRollData(
|
|||
};
|
||||
const renderedHtml = await renderTemplate(usedTemplate, templateData);
|
||||
|
||||
const dialogPromise = new Promise<HTMLFormElement>((resolve) => {
|
||||
const dialogPromise = new Promise((resolve) => {
|
||||
new DialogWithListeners(
|
||||
{
|
||||
title: usedTitle,
|
||||
|
@ -190,7 +214,7 @@ async function askForInteractiveRollData(
|
|||
.parent(".form-group");
|
||||
html.find(`#check-modifier-${id}`).on("change", (event) => {
|
||||
if (
|
||||
(event.currentTarget as HTMLSelectElement).value === "custom" &&
|
||||
event.currentTarget.value === "custom" &&
|
||||
checkModifierCustomFormGroup.hasClass("ds4-hidden")
|
||||
) {
|
||||
checkModifierCustomFormGroup.removeClass("ds4-hidden");
|
||||
|
@ -211,9 +235,10 @@ async function askForInteractiveRollData(
|
|||
|
||||
/**
|
||||
* Extracts Dialog data from the returned DOM element.
|
||||
* @param formData - The filed dialog
|
||||
* @param {HTMLFormElement} formData The filed dialog
|
||||
* @returns {Partial<IntermediateInteractiveRollData>}
|
||||
*/
|
||||
function parseDialogFormData(formData: HTMLFormElement): Partial<IntermediateInteractiveRollData> {
|
||||
function parseDialogFormData(formData) {
|
||||
const chosenCheckTargetNumber = parseInt(formData["check-target-number"]?.value);
|
||||
const chosenCheckModifier = formData["check-modifier"]?.value;
|
||||
|
||||
|
@ -237,11 +262,10 @@ function parseDialogFormData(formData: HTMLFormElement): Partial<IntermediateInt
|
|||
|
||||
/**
|
||||
* Contains data that needs retrieval from an interactive Dialog.
|
||||
* @typedef {object} InteractiveRollData
|
||||
* @property {number} checkModifier
|
||||
* @property {RollModes} rollMode
|
||||
*/
|
||||
interface InteractiveRollData {
|
||||
checkModifier: number;
|
||||
rollMode: keyof CONFIG.Dice.RollModes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains *CURRENTLY* necessary Data for drafting a roll.
|
||||
|
@ -255,23 +279,29 @@ interface InteractiveRollData {
|
|||
* 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 `InteractiveRollData`.
|
||||
* This interface should then be replaced with the {@link InteractiveRollData}.
|
||||
* @typedef {object} IntermediateInteractiveRollData
|
||||
* @property {number} checkTargetNumber
|
||||
* @property {number} maximumCoupResult
|
||||
* @property {number} minimumFumbleResult
|
||||
*/
|
||||
interface IntermediateInteractiveRollData extends InteractiveRollData {
|
||||
checkTargetNumber: number;
|
||||
maximumCoupResult: number;
|
||||
minimumFumbleResult: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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]
|
||||
*/
|
||||
export interface DS4CheckFactoryOptions {
|
||||
maximumCoupResult: number;
|
||||
minimumFumbleResult: number;
|
||||
useSlayingDice: boolean;
|
||||
rollMode: keyof CONFIG.Dice.RollModes;
|
||||
flavor?: string;
|
||||
flavorData?: Record<string, string | number | null>;
|
||||
speaker?: ReturnType<typeof ChatMessage.getSpeaker>;
|
||||
}
|
|
@ -16,7 +16,7 @@ import { evaluateCheck, getRequiredNumberOfDice } from "./check-evaluation";
|
|||
* - Roll a check with a racial ability that makes `5` a coup and default fumble: `/r dsv19c5`
|
||||
*/
|
||||
export class DS4Check extends DiceTerm {
|
||||
constructor({ modifiers = [], results = [], options }: Partial<DiceTerm.TermData> = {}) {
|
||||
constructor({ modifiers = [], results = [], options } = {}) {
|
||||
super({
|
||||
faces: 20,
|
||||
results,
|
||||
|
@ -65,34 +65,44 @@ export class DS4Check extends DiceTerm {
|
|||
}
|
||||
}
|
||||
|
||||
coup: boolean | null = null;
|
||||
fumble: boolean | null = null;
|
||||
/** @type {boolean | null} */
|
||||
coup = null;
|
||||
/** @type {boolean | null} */
|
||||
fumble = null;
|
||||
canFumble = true;
|
||||
checkTargetNumber = DS4Check.DEFAULT_CHECK_TARGET_NUMBER;
|
||||
minimumFumbleResult = DS4Check.DEFAULT_MINIMUM_FUMBLE_RESULT;
|
||||
maximumCoupResult = DS4Check.DEFAULT_MAXIMUM_COUP_RESULT;
|
||||
|
||||
override get expression(): string {
|
||||
/** @override */
|
||||
get expression() {
|
||||
return `ds${this.modifiers.join("")}`;
|
||||
}
|
||||
|
||||
override get total(): string | number | null | undefined {
|
||||
/** @override */
|
||||
get total() {
|
||||
if (this.fumble) return 0;
|
||||
return super.total;
|
||||
}
|
||||
|
||||
override _evaluateSync({ minimize = false, maximize = false } = {}): this {
|
||||
/** @override */
|
||||
_evaluateSync({ minimize = false, maximize = false } = {}) {
|
||||
super._evaluateSync({ minimize, maximize });
|
||||
this.evaluateResults();
|
||||
return this;
|
||||
}
|
||||
|
||||
override roll({ minimize = false, maximize = false } = {}): DiceTerm.Result {
|
||||
/** @override */
|
||||
roll({ minimize = false, maximize = false } = {}) {
|
||||
// Swap minimize / maximize because in DS4, the best possible roll is a 1 and the worst possible roll is a 20
|
||||
return super.roll({ minimize: maximize, maximize: minimize });
|
||||
}
|
||||
|
||||
evaluateResults(): void {
|
||||
/**
|
||||
* Perform additional evaluation after the individual results have been evaluated.
|
||||
* @protected
|
||||
*/
|
||||
evaluateResults() {
|
||||
const dice = this.results.map((die) => die.result);
|
||||
const results = evaluateCheck(dice, this.checkTargetNumber, {
|
||||
maximumCoupResult: this.maximumCoupResult,
|
||||
|
@ -108,18 +118,19 @@ export class DS4Check extends DiceTerm {
|
|||
* @remarks "min" and "max" are filtered out because they are irrelevant for
|
||||
* {@link DS4Check}s and only result in some dice rolls being highlighted
|
||||
* incorrectly.
|
||||
* @override
|
||||
*/
|
||||
override getResultCSS(result: DiceTerm.Result): (string | null)[] {
|
||||
getResultCSS(result) {
|
||||
return super.getResultCSS(result).filter((cssClass) => cssClass !== "min" && cssClass !== "max");
|
||||
}
|
||||
|
||||
static readonly DEFAULT_CHECK_TARGET_NUMBER = 10;
|
||||
static readonly DEFAULT_MAXIMUM_COUP_RESULT = 1;
|
||||
static readonly DEFAULT_MINIMUM_FUMBLE_RESULT = 20;
|
||||
static override DENOMINATION = "s";
|
||||
static override MODIFIERS = {
|
||||
c: (): void => undefined, // Modifier is consumed in constructor for maximumCoupResult / minimumFumbleResult
|
||||
v: (): void => undefined, // Modifier is consumed in constructor for checkTargetNumber
|
||||
n: (): void => undefined, // Modifier is consumed in constructor for canFumble
|
||||
static DEFAULT_CHECK_TARGET_NUMBER = 10;
|
||||
static DEFAULT_MAXIMUM_COUP_RESULT = 1;
|
||||
static DEFAULT_MINIMUM_FUMBLE_RESULT = 20;
|
||||
static DENOMINATION = "s";
|
||||
static MODIFIERS = {
|
||||
c: () => undefined, // Modifier is consumed in constructor for maximumCoupResult / minimumFumbleResult
|
||||
v: () => undefined, // Modifier is consumed in constructor for checkTargetNumber
|
||||
n: () => undefined, // Modifier is consumed in constructor for canFumble
|
||||
};
|
||||
}
|
|
@ -5,19 +5,17 @@
|
|||
import { getGame } from "../utils/utils";
|
||||
import { DS4Check } from "./check";
|
||||
|
||||
export class DS4Roll<D extends Record<string, unknown> = Record<string, unknown>> extends Roll<D> {
|
||||
static override CHAT_TEMPLATE = "systems/ds4/templates/dice/roll.hbs";
|
||||
export class DS4Roll extends Roll {
|
||||
/** @override */
|
||||
static CHAT_TEMPLATE = "systems/ds4/templates/dice/roll.hbs";
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @remarks
|
||||
* This only differs from {@link Roll#render} in that it provides `isCoup` and `isFumble` properties to the roll
|
||||
* template if the first dice term is a ds4 check.
|
||||
*/
|
||||
override async render({
|
||||
flavor,
|
||||
template = (this.constructor as typeof DS4Roll).CHAT_TEMPLATE,
|
||||
isPrivate = false,
|
||||
}: Parameters<Roll["render"]>[0] = {}): Promise<string> {
|
||||
async render({ flavor, template = this.constructor.CHAT_TEMPLATE, isPrivate = false } = {}) {
|
||||
if (!this._evaluated) await this.evaluate({ async: true });
|
||||
const firstDiceTerm = this.dice[0];
|
||||
const isCoup = firstDiceTerm instanceof DS4Check && firstDiceTerm.coup;
|
|
@ -6,11 +6,15 @@
|
|||
import { getGame } from "../utils/utils";
|
||||
import { DS4Check } from "./check";
|
||||
|
||||
export function registerSlayingDiceModifier(): void {
|
||||
export function registerSlayingDiceModifier() {
|
||||
PoolTerm.MODIFIERS.x = slay;
|
||||
}
|
||||
|
||||
function slay(this: PoolTerm, modifier: string): void {
|
||||
/**
|
||||
* @this {PoolTerm}
|
||||
* @param {string} modifier
|
||||
*/
|
||||
function slay(modifier) {
|
||||
const rgx = /[xX]/;
|
||||
const match = modifier.match(rgx);
|
||||
if (!match || !this.rolls) return;
|
Loading…
Add table
Add a link
Reference in a new issue