// SPDX-FileCopyrightText: 2021 Johannes Loher // SPDX-FileCopyrightText: 2021 Oliver Rümpelein // // SPDX-License-Identifier: MIT 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 = [], options }: Partial = {}) { super({ faces: 20, modifiers: modifiers, options: 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(game.i18n.localize("DS4.ErrorDiceCoupFumbleOverlap")); } // Parse and store no fumble const noFumbleModifier = this.modifiers.filter((m) => m[0] === "n")[0]; if (noFumbleModifier) { this.canFumble = false; } } 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 */ evaluate({ minimize = false, maximize = false } = {}): this { super.evaluate({ 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 */ static fromResults( this: ConstructorOf, options: Partial, results: DiceTerm.Result[], ): T { const term = new this(options); term.results = results; term.evaluateResults(); term._evaluated = true; return term; } 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 }; }