141 lines
5.3 KiB
TypeScript
141 lines
5.3 KiB
TypeScript
import { RollResult, RollResultStatus } from "./roll-data";
|
|
import { ds4roll } from "./roll-executor";
|
|
|
|
interface TermData {
|
|
number: number;
|
|
faces: number;
|
|
modifiers: Array<string>;
|
|
options: Record<string, unknown>;
|
|
}
|
|
|
|
/**
|
|
* 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<TermData>) {
|
|
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: boolean | null = null;
|
|
failure: boolean | null = 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(): void {
|
|
return;
|
|
}
|
|
|
|
// 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_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
|
|
};
|
|
}
|