refactor: resturcture files so that lincensing info can be bundled properly
This commit is contained in:
parent
699ba74840
commit
1aa284311f
484 changed files with 119 additions and 179 deletions
131
src/rolls/check-evaluation.ts
Normal file
131
src/rolls/check-evaluation.ts
Normal file
|
@ -0,0 +1,131 @@
|
|||
// SPDX-FileCopyrightText: 2021 Johannes Loher
|
||||
// SPDX-FileCopyrightText: 2021 Oliver Rümpelein
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { getGame } from "../helpers";
|
||||
|
||||
export default function evaluateCheck(
|
||||
dice: number[],
|
||||
checkTargetNumber: number,
|
||||
{
|
||||
maximumCoupResult = 1,
|
||||
minimumFumbleResult = 20,
|
||||
canFumble = true,
|
||||
}: { maximumCoupResult?: number; minimumFumbleResult?: number; canFumble?: boolean } = {},
|
||||
): SubCheckResult[] {
|
||||
const diceWithSubChecks = assignSubChecksToDice(dice, checkTargetNumber, {
|
||||
maximumCoupResult: maximumCoupResult,
|
||||
});
|
||||
return evaluateDiceWithSubChecks(diceWithSubChecks, {
|
||||
maximumCoupResult: maximumCoupResult,
|
||||
minimumFumbleResult: minimumFumbleResult,
|
||||
canFumble: canFumble,
|
||||
});
|
||||
}
|
||||
|
||||
interface DieWithSubCheck {
|
||||
result: number;
|
||||
checkTargetNumber: number;
|
||||
}
|
||||
|
||||
function assignSubChecksToDice(
|
||||
dice: number[],
|
||||
checkTargetNumber: number,
|
||||
{
|
||||
maximumCoupResult = 1,
|
||||
}: {
|
||||
maximumCoupResult?: number;
|
||||
} = {},
|
||||
): DieWithSubCheck[] {
|
||||
const requiredNumberOfDice = getRequiredNumberOfDice(checkTargetNumber);
|
||||
|
||||
if (dice.length !== requiredNumberOfDice || requiredNumberOfDice < 1) {
|
||||
throw new Error(getGame().i18n.localize("DS4.ErrorInvalidNumberOfDice"));
|
||||
}
|
||||
|
||||
const checkTargetNumberForLastSubCheck = checkTargetNumber - 20 * (requiredNumberOfDice - 1);
|
||||
|
||||
const indexOfSmallestNonCoup = findIndexOfSmallestNonCoup(dice, maximumCoupResult);
|
||||
const indexOfFirstCoup = dice.findIndex((die) => die <= maximumCoupResult);
|
||||
const indexForLastSubCheck = shouldUseCoupForLastSubCheck(
|
||||
indexOfSmallestNonCoup,
|
||||
indexOfFirstCoup,
|
||||
dice,
|
||||
checkTargetNumberForLastSubCheck,
|
||||
)
|
||||
? indexOfFirstCoup
|
||||
: indexOfSmallestNonCoup;
|
||||
|
||||
return dice.map((die, index) => ({
|
||||
result: die,
|
||||
checkTargetNumber: index === indexForLastSubCheck ? checkTargetNumberForLastSubCheck : 20,
|
||||
}));
|
||||
}
|
||||
|
||||
function findIndexOfSmallestNonCoup(dice: number[], maximumCoupResult: number): number {
|
||||
return dice
|
||||
.map((die, index): [number, number] => [die, index])
|
||||
.filter((indexedDie) => indexedDie[0] > maximumCoupResult)
|
||||
.reduce(
|
||||
(smallestIndexedDie, indexedDie) =>
|
||||
indexedDie[0] < smallestIndexedDie[0] ? indexedDie : smallestIndexedDie,
|
||||
[Infinity, -1],
|
||||
)[1];
|
||||
}
|
||||
|
||||
function shouldUseCoupForLastSubCheck(
|
||||
indexOfSmallestNonCoup: number,
|
||||
indexOfFirstCoup: number,
|
||||
dice: readonly number[],
|
||||
checkTargetNumberForLastSubCheck: number,
|
||||
) {
|
||||
if (indexOfFirstCoup !== -1 && indexOfSmallestNonCoup === -1) {
|
||||
return true;
|
||||
}
|
||||
const smallestNonCoup = dice[indexOfSmallestNonCoup];
|
||||
if (
|
||||
!Number.isInteger(indexOfFirstCoup) ||
|
||||
indexOfFirstCoup < -1 ||
|
||||
!Number.isInteger(indexOfSmallestNonCoup) ||
|
||||
smallestNonCoup === undefined
|
||||
) {
|
||||
throw new Error("Received an invalid value for the parameter indexOfSmallestNonCoup or indexOfFirstCoup,");
|
||||
}
|
||||
return (
|
||||
indexOfFirstCoup !== -1 &&
|
||||
smallestNonCoup > checkTargetNumberForLastSubCheck &&
|
||||
smallestNonCoup + checkTargetNumberForLastSubCheck > 20
|
||||
);
|
||||
}
|
||||
|
||||
interface SubCheckResult extends DieWithSubCheck, DiceTerm.Result {}
|
||||
|
||||
function evaluateDiceWithSubChecks(
|
||||
results: DieWithSubCheck[],
|
||||
{
|
||||
maximumCoupResult,
|
||||
minimumFumbleResult,
|
||||
canFumble,
|
||||
}: { maximumCoupResult: number; minimumFumbleResult: number; canFumble: boolean },
|
||||
): SubCheckResult[] {
|
||||
return results.map((dieWithSubCheck, index) => {
|
||||
const result: SubCheckResult = {
|
||||
...dieWithSubCheck,
|
||||
active: dieWithSubCheck.result <= dieWithSubCheck.checkTargetNumber,
|
||||
discarded: dieWithSubCheck.result > dieWithSubCheck.checkTargetNumber,
|
||||
};
|
||||
if (result.result <= maximumCoupResult) {
|
||||
result.success = true;
|
||||
result.count = result.checkTargetNumber;
|
||||
result.active = true;
|
||||
result.discarded = false;
|
||||
}
|
||||
if (index === 0 && canFumble && result.result >= minimumFumbleResult) result.failure = true;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
export function getRequiredNumberOfDice(checkTargetNumber: number): number {
|
||||
return Math.max(Math.ceil(checkTargetNumber / 20), 1);
|
||||
}
|
236
src/rolls/check-factory.ts
Normal file
236
src/rolls/check-factory.ts
Normal file
|
@ -0,0 +1,236 @@
|
|||
// SPDX-FileCopyrightText: 2021 Johannes Loher
|
||||
// SPDX-FileCopyrightText: 2021 Oliver Rümpelein
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { getGame } from "../helpers";
|
||||
|
||||
/**
|
||||
* Provides default values for all arguments the `CheckFactory` expects.
|
||||
*/
|
||||
class DefaultCheckOptions implements DS4CheckFactoryOptions {
|
||||
readonly maximumCoupResult = 1;
|
||||
readonly minimumFumbleResult = 20;
|
||||
readonly useSlayingDice = false;
|
||||
// TODO: Use the type `keyof CONFIG.Dice.rollModes` instead as soon as https://github.com/League-of-Foundry-Developers/foundry-vtt-types/issues/1501 has been resolved.
|
||||
readonly rollMode: foundry.CONST.DICE_ROLL_MODES = "publicroll";
|
||||
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 | undefined> {
|
||||
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 },
|
||||
);
|
||||
}
|
||||
|
||||
createCheckTargetNumberModifier(): string {
|
||||
const totalCheckTargetNumber = Math.max(this.checkTargetNumber + this.gmModifier, 0);
|
||||
return `v${totalCheckTargetNumber}`;
|
||||
}
|
||||
|
||||
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: getGame().settings.get("ds4", "useSlayingDiceForAutomatedChecks"),
|
||||
rollMode: gmModifierData.rollMode ?? options.rollMode,
|
||||
flavor: options.flavor,
|
||||
flavorData: options.flavorData,
|
||||
speaker: options.speaker,
|
||||
};
|
||||
|
||||
// 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 ?? getGame().i18n.localize("DS4.DialogRollOptionsDefaultTitle");
|
||||
const templateData = {
|
||||
title: usedTitle,
|
||||
checkTargetNumber: checkTargetNumber,
|
||||
maximumCoupResult: options.maximumCoupResult ?? defaultCheckOptions.maximumCoupResult,
|
||||
minimumFumbleResult: options.minimumFumbleResult ?? defaultCheckOptions.minimumFumbleResult,
|
||||
rollMode: options.rollMode ?? getGame().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: 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",
|
||||
}).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> {
|
||||
const chosenCheckTargetNumber = parseInt(formData["check-target-number"]?.value);
|
||||
const chosenGMModifier = parseInt(formData["gm-modifier"]?.value);
|
||||
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,
|
||||
gmModifier: Number.isSafeInteger(chosenGMModifier) ? chosenGMModifier : 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.
|
||||
*/
|
||||
interface GmModifierData {
|
||||
gmModifier: number;
|
||||
rollMode: foundry.CONST.DICE_ROLL_MODES;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: foundry.CONST.DICE_ROLL_MODES;
|
||||
flavor?: string;
|
||||
flavorData?: Record<string, string | number | null>;
|
||||
speaker?: ReturnType<typeof ChatMessage.getSpeaker>;
|
||||
}
|
130
src/rolls/check.ts
Normal file
130
src/rolls/check.ts
Normal file
|
@ -0,0 +1,130 @@
|
|||
// SPDX-FileCopyrightText: 2021 Johannes Loher
|
||||
// SPDX-FileCopyrightText: 2021 Oliver Rümpelein
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { getGame } from "../helpers";
|
||||
import evaluateCheck, { getRequiredNumberOfDice } from "./check-evaluation";
|
||||
|
||||
/**
|
||||
* Implements DS4 Checks as an emulated "dice throw".
|
||||
*
|
||||
* @example
|
||||
* - Roll a check against a Check Target Number (CTN) of 18: `/r dsv18`
|
||||
* - Roll a check with multiple dice against a CTN of 34: `/r dsv34`
|
||||
* - Roll a check with a racial ability that makes `2` a coup and `19` a fumble: `/r dsv19c2:19`
|
||||
* - 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> = {}) {
|
||||
super({
|
||||
faces: 20,
|
||||
results,
|
||||
modifiers,
|
||||
options,
|
||||
});
|
||||
|
||||
// Parse and store check target number
|
||||
const checkTargetNumberModifier = this.modifiers.filter((m) => m[0] === "v")[0];
|
||||
const ctnRgx = new RegExp("v([0-9]+)?");
|
||||
const ctnMatch = checkTargetNumberModifier?.match(ctnRgx);
|
||||
if (ctnMatch) {
|
||||
const [parseCheckTargetNumber] = ctnMatch.slice(1);
|
||||
this.checkTargetNumber = parseCheckTargetNumber
|
||||
? parseInt(parseCheckTargetNumber)
|
||||
: DS4Check.DEFAULT_CHECK_TARGET_NUMBER;
|
||||
}
|
||||
|
||||
this.number = getRequiredNumberOfDice(this.checkTargetNumber);
|
||||
|
||||
// Parse and store maximumCoupResult and minimumFumbleResult
|
||||
const coupFumbleModifier = this.modifiers.filter((m) => m[0] === "c")[0];
|
||||
const cfmRgx = new RegExp("c([0-9]+)?(:([0-9]+))?");
|
||||
const cfmMatch = coupFumbleModifier?.match(cfmRgx);
|
||||
if (cfmMatch) {
|
||||
const parseMaximumCoupResult = cfmMatch[1];
|
||||
const parseMinimumFumbleResult = cfmMatch[3];
|
||||
this.maximumCoupResult = parseMaximumCoupResult
|
||||
? parseInt(parseMaximumCoupResult)
|
||||
: DS4Check.DEFAULT_MAXIMUM_COUP_RESULT;
|
||||
this.minimumFumbleResult = parseMinimumFumbleResult
|
||||
? parseInt(parseMinimumFumbleResult)
|
||||
: DS4Check.DEFAULT_MINIMUM_FUMBLE_RESULT;
|
||||
if (this.minimumFumbleResult <= this.maximumCoupResult)
|
||||
throw new SyntaxError(getGame().i18n.localize("DS4.ErrorDiceCoupFumbleOverlap"));
|
||||
}
|
||||
|
||||
// Parse and store no fumble
|
||||
const noFumbleModifier = this.modifiers.filter((m) => m[0] === "n")[0];
|
||||
if (noFumbleModifier) {
|
||||
this.canFumble = false;
|
||||
}
|
||||
|
||||
if (this.results.length > 0) {
|
||||
this.evaluateResults();
|
||||
}
|
||||
}
|
||||
|
||||
coup: boolean | null = null;
|
||||
fumble: boolean | null = 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 {
|
||||
return `ds${this.modifiers.join("")}`;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
get total(): string | number | null | undefined {
|
||||
if (this.fumble) return 0;
|
||||
return super.total;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_evaluateSync({ minimize = false, maximize = false } = {}): this {
|
||||
super._evaluateSync({ minimize, maximize });
|
||||
this.evaluateResults();
|
||||
return this;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
roll({ minimize = false, maximize = false } = {}): DiceTerm.Result {
|
||||
// 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 {
|
||||
const dice = this.results.map((die) => die.result);
|
||||
const results = evaluateCheck(dice, this.checkTargetNumber, {
|
||||
maximumCoupResult: this.maximumCoupResult,
|
||||
minimumFumbleResult: this.minimumFumbleResult,
|
||||
canFumble: this.canFumble,
|
||||
});
|
||||
this.results = results;
|
||||
this.coup = results[0]?.success ?? false;
|
||||
this.fumble = results[0]?.failure ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @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.
|
||||
*/
|
||||
getResultCSS(result: DiceTerm.Result): (string | null)[] {
|
||||
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 DENOMINATION = "s";
|
||||
static 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
|
||||
};
|
||||
}
|
37
src/rolls/roll.ts
Normal file
37
src/rolls/roll.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
// SPDX-FileCopyrightText: 2021 Johannes Loher
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { getGame } from "../helpers";
|
||||
import { DS4Check } from "./check";
|
||||
|
||||
export class DS4Roll<D extends Record<string, unknown> = Record<string, unknown>> extends Roll<D> {
|
||||
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.
|
||||
*/
|
||||
async render({
|
||||
flavor,
|
||||
template = (this.constructor as typeof DS4Roll).CHAT_TEMPLATE,
|
||||
isPrivate = false,
|
||||
}: Parameters<Roll["render"]>[0] = {}): Promise<string> {
|
||||
if (!this._evaluated) await this.evaluate({ async: true });
|
||||
const firstDiceTerm = this.dice[0];
|
||||
const isCoup = firstDiceTerm instanceof DS4Check && firstDiceTerm.coup;
|
||||
const isFumble = firstDiceTerm instanceof DS4Check && firstDiceTerm.fumble;
|
||||
const chatData = {
|
||||
formula: isPrivate ? "???" : this._formula,
|
||||
flavor: isPrivate ? null : flavor,
|
||||
user: getGame().user?.id,
|
||||
tooltip: isPrivate ? "" : await this.getTooltip(),
|
||||
total: isPrivate ? "?" : Math.round((this.total ?? 0) * 100) / 100,
|
||||
isCoup: isPrivate ? null : isCoup,
|
||||
isFumble: isPrivate ? null : isFumble,
|
||||
};
|
||||
return renderTemplate(template, chatData);
|
||||
}
|
||||
}
|
32
src/rolls/slaying-dice-modifier.ts
Normal file
32
src/rolls/slaying-dice-modifier.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
// SPDX-FileCopyrightText: 2021 Johannes Loher
|
||||
// SPDX-FileCopyrightText: 2021 Oliver Rümpelein
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { getGame } from "../helpers";
|
||||
import { DS4Check } from "./check";
|
||||
|
||||
export default function registerSlayingDiceModifier(): void {
|
||||
PoolTerm.MODIFIERS.x = slay;
|
||||
}
|
||||
|
||||
function slay(this: PoolTerm, modifier: string): void {
|
||||
const rgx = /[xX]/;
|
||||
const match = modifier.match(rgx);
|
||||
if (!match || !this.rolls) return;
|
||||
|
||||
let checked = 0;
|
||||
while (checked < (this.dice.length ?? 0)) {
|
||||
const diceTerm = this.dice[checked];
|
||||
checked++;
|
||||
if (diceTerm instanceof DS4Check && diceTerm.coup) {
|
||||
const formula = `dsv${diceTerm.checkTargetNumber}c${diceTerm.maximumCoupResult}:${diceTerm.minimumFumbleResult}n`;
|
||||
const additionalRoll = Roll.create(formula).evaluate({ async: false });
|
||||
|
||||
this.rolls.push(additionalRoll);
|
||||
this.results.push({ result: additionalRoll.total ?? 0, active: true });
|
||||
this.terms.push(formula);
|
||||
}
|
||||
if (checked > 1000) throw new Error(getGame().i18n.localize("DS4.ErrorSlayingDiceRecursionLimitExceeded"));
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue