import evaluateCheck, { getRequiredNumberOfDice } from "./check-evaluation"; interface DS4CheckTermData extends DiceTerm.TermData { canFumble: boolean; } /** * 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 = [], options = {}, canFumble = true }: Partial = {}) { super({ faces: 20, modifiers: modifiers, options: options, }); this.canFumble = canFumble; // Parse and store check target number const targetValueModifier = this.modifiers.filter((m) => m[0] === "v")[0]; const tvRgx = new RegExp("v([0-9]+)?"); const tvMatch = targetValueModifier?.match(tvRgx); if (tvMatch) { const [parseTargetValue] = tvMatch.slice(1); this.checkTargetNumber = parseTargetValue ? parseInt(parseTargetValue) : DS4Check.DEFAULT_CHECK_TARGET_NUMBER; } this.number = getRequiredNumberOfDice(this.checkTargetNumber); // Parse and store minimumCoupResult and maximumFumbleResult 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, parseMinimumFumbleResult] = cfmMatch.slice(1); 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(game.i18n.localize("DS4.ErrorDiceCritOverlap")); } } coup: boolean | null = null; fumble: boolean | null = null; canFumble: boolean; 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(): number | null { if (this.fumble) return 0; return super.total; } /** * @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; } // // DS4 only allows recursive explosions // explode(modifier: string): void { // const rgx = /[xX]/; // const match = modifier.match(rgx); // if (!match) return; // this.results = (this.results as Array) // .map((r) => { // const intermediateResults = [r]; // let checked = 0; // while (checked < intermediateResults.length) { // const r = intermediateResults[checked]; // checked++; // if (!r.active) continue; // if (r.dice[0] <= this.maxCritSuccess) { // r.exploded = true; // const newRoll = this.rollWithDifferentBorders({}, true); // intermediateResults.push(newRoll); // } // if (checked > 1000) throw new Error(game.i18n.localize("DS4.ErrorExplodingRecursionLimitExceeded")); // } // return intermediateResults; // }) // .reduce((acc, cur) => { // return acc.concat(cur); // }, []); // } 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 = { //x: "explode", c: (): void => undefined, // Modifier is consumed in constructor for crit v: "evaluateResults", }; }