ds4/src/module/rolls/check.ts

139 lines
5.1 KiB
TypeScript
Raw Normal View History

import evaluateCheck, { getRequiredNumberOfDice } from "./check-evaluation";
interface DS4CheckTermData extends DiceTerm.TermData {
canFumble: boolean;
2021-01-08 23:18:01 +01:00
}
2021-01-08 23:41:23 +01:00
/**
* Implements DS4 Checks as an emulated "dice throw".
*
* @example
* - Roll a check against a Check Target Number (CTN) of 18: `/r dsv18`
2021-01-08 23:41:23 +01:00
* - 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`
*/
2021-01-08 23:18:01 +01:00
export class DS4Check extends DiceTerm {
constructor({ modifiers = [], options = {}, canFumble = true }: Partial<DS4CheckTermData> = {}) {
2021-01-08 23:18:01 +01:00
super({
faces: 20,
modifiers: modifiers,
options: options,
2021-01-08 23:18:01 +01:00
});
this.canFumble = canFumble;
// Parse and store check target number
2021-01-08 23:18:01 +01:00
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;
2021-01-08 23:18:01 +01:00
}
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)
2021-01-08 23:31:42 +01:00
throw new SyntaxError(game.i18n.localize("DS4.ErrorDiceCritOverlap"));
2021-01-08 23:18:01 +01:00
}
}
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;
2021-01-08 23:18:01 +01:00
/**
* @override
*/
get expression(): string {
return `ds${this.modifiers.join("")}`;
2021-01-08 23:18:01 +01:00
}
/**
* @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 });
2021-01-08 23:18:01 +01:00
}
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;
2021-01-08 23:18:01 +01:00
}
// // 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<RollResult>)
// .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;
2021-01-08 23:18:01 +01:00
static DENOMINATION = "s";
static MODIFIERS = {
//x: "explode",
2021-02-08 02:15:43 +01:00
c: (): void => undefined, // Modifier is consumed in constructor for crit
v: "evaluateResults",
2021-01-08 23:18:01 +01:00
};
}