import { RollResult, RollResultStatus } from "./roll-data"; import { ds4roll } from "./roll-executor"; interface TermData { number: number; faces: number; modifiers: Array; options: Record; } /** * Implements DS4 Checks as an emulated "dice throw". * * @notes * Be aware that, even though this behaves like one roll, it actually throws several ones internally * * @example * - Roll a check against a Check Target Number (CTV) 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` * - Roll a check with exploding dice: `/r dsv34x` */ export class DS4Check extends DiceTerm { constructor(termData: Partial) { super({ number: termData.number, faces: termData.faces, // should be null modifiers: termData.modifiers ?? [], options: termData.options ?? {}, }); // Store and parse target value. 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.targetValue = parseTargetValue ? parseInt(parseTargetValue) : DS4Check.DEFAULT_TARGET_VALUE; } // Store and parse min/max crit const critModifier = this.modifiers.filter((m) => m[0] === "c")[0]; const cmRgx = new RegExp("c([0-9]+)?,([0-9]+)?"); const cmMatch = critModifier?.match(cmRgx); if (cmMatch) { const [parseMaxCritSuccess, parseMinCritFailure] = cmMatch.slice(1); this.maxCritSuccess = parseMaxCritSuccess ? parseInt(parseMaxCritSuccess) : DS4Check.DEFAULT_MAX_CRIT_SUCCESS; this.minCritFailure = parseMinCritFailure ? parseInt(parseMinCritFailure) : DS4Check.DEFAULT_MIN_CRIT_FAILURE; if (this.minCritFailure <= this.maxCritSuccess) throw new SyntaxError(game.i18n.localize("DS4.ErrorDiceCritOverlap")); } } success = null; failure = null; targetValue = DS4Check.DEFAULT_TARGET_VALUE; minCritFailure = DS4Check.DEFAULT_MIN_CRIT_FAILURE; maxCritSuccess = DS4Check.DEFAULT_MAX_CRIT_SUCCESS; /** * @override */ roll({ minimize = false, maximize = false } = {}): RollResult { const rollResult = this.rollWithDifferentBorders({ minimize, maximize }); this.results.push(rollResult); if (rollResult.status == RollResultStatus.CRITICAL_SUCCESS) { this.success = true; } else if (rollResult.status == RollResultStatus.CRITICAL_FAILURE) { this.failure = true; } return rollResult; } rollWithDifferentBorders({ minimize = false, maximize = false } = {}, slayingDiceRepetition = false): RollResult { const targetValueToUse = this.targetValue; if (minimize) { return new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [20], true); } else if (maximize) { const maximizedDice = Array(Math.ceil(targetValueToUse / 20)).fill(1); return new RollResult(targetValueToUse, RollResultStatus.CRITICAL_SUCCESS, maximizedDice, true); } else { return ds4roll(targetValueToUse, { maxCritSuccess: this.maxCritSuccess, minCritFailure: this.minCritFailure, slayingDiceRepetition: slayingDiceRepetition, useSlayingDice: slayingDiceRepetition, }); } } /** Term Modifiers */ noop(): this { return this; } // DS4 only allows recursive explosions explode(modifier: string): this { const rgx = /[xX]/; const match = modifier.match(rgx); if (!match) return this; 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_TARGET_VALUE = 10; static readonly DEFAULT_MAX_CRIT_SUCCESS = 1; static readonly DEFAULT_MIN_CRIT_FAILURE = 20; static DENOMINATION = "s"; static MODIFIERS = { x: "explode", c: "noop", // Modifier is consumed in constructor for target value v: "noop", // Modifier is consumed in constructor for target value }; }