ds4/src/module/rolls/check.ts

131 lines
5 KiB
TypeScript
Raw Normal View History

2021-06-26 22:02:00 +02:00
// SPDX-FileCopyrightText: 2021 Johannes Loher
// SPDX-FileCopyrightText: 2021 Oliver Rümpelein
//
// SPDX-License-Identifier: MIT
2021-07-07 19:22:35 +02:00
import { getGame } from "../helpers";
import evaluateCheck, { getRequiredNumberOfDice } from "./check-evaluation";
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`
2021-03-13 22:08:04 +01:00
* - Roll a check with a racial ability that makes `2` a coup and `19` a fumble: `/r dsv19c2:19`
2021-01-08 23:41:23 +01:00
* - 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 {
2021-07-01 00:36:41 +02:00
constructor({ modifiers = [], results = [], options }: Partial<DiceTerm.TermData> = {}) {
2021-01-08 23:18:01 +01:00
super({
faces: 20,
2021-07-01 00:36:41 +02:00
results,
modifiers,
options,
2021-01-08 23:18:01 +01:00
});
// Parse and store check target number
2021-03-13 18:50:39 +01:00
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;
2021-01-08 23:18:01 +01:00
}
this.number = getRequiredNumberOfDice(this.checkTargetNumber);
2021-03-13 17:47:47 +01:00
// Parse and store maximumCoupResult and minimumFumbleResult
const coupFumbleModifier = this.modifiers.filter((m) => m[0] === "c")[0];
2021-03-13 22:08:04 +01:00
const cfmRgx = new RegExp("c([0-9]+)?(:([0-9]+))?");
const cfmMatch = coupFumbleModifier?.match(cfmRgx);
if (cfmMatch) {
2021-03-13 22:08:04 +01:00
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)
2021-07-07 19:22:35 +02:00
throw new SyntaxError(getGame().i18n.localize("DS4.ErrorDiceCoupFumbleOverlap"));
2021-01-08 23:18:01 +01:00
}
2021-03-13 22:08:04 +01:00
// Parse and store no fumble
const noFumbleModifier = this.modifiers.filter((m) => m[0] === "n")[0];
if (noFumbleModifier) {
this.canFumble = false;
}
2021-07-01 00:36:41 +02:00
if (this.results.length > 0) {
this.evaluateResults();
}
2021-01-08 23:18:01 +01:00
}
coup: boolean | null = null;
fumble: boolean | null = null;
2021-03-13 22:08:04 +01:00
canFumble = true;
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
2021-03-14 14:52:50 +01:00
/** @override */
get expression(): string {
return `ds${this.modifiers.join("")}`;
2021-01-08 23:18:01 +01:00
}
2021-03-14 14:52:50 +01:00
/** @override */
2021-06-30 04:32:10 +02:00
get total(): string | number | null | undefined {
if (this.fumble) return 0;
return super.total;
}
2021-03-14 14:52:50 +01:00
/** @override */
2021-07-01 00:36:41 +02:00
_evaluateSync({ minimize = false, maximize = false } = {}): this {
super._evaluateSync({ minimize, maximize });
this.evaluateResults();
return this;
}
2021-03-14 14:52:50 +01:00
/** @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
}
/**
* @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;
2021-01-08 23:18:01 +01:00
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
2021-03-13 22:08:04 +01:00
n: (): void => undefined, // Modifier is consumed in constructor for canFumble
2021-01-08 23:18:01 +01:00
};
}