ds4/src/module/rolls/check.ts

142 lines
5.3 KiB
TypeScript
Raw Normal View History

2021-01-08 23:18:01 +01:00
import { RollResult, RollResultStatus } from "./roll-data";
import { ds4roll } from "./roll-executor";
interface TermData {
number: number;
faces: number;
modifiers: Array<string>;
options: Record<string, unknown>;
}
2021-01-08 23:41:23 +01:00
/**
* Implements DS4 Checks as an emulated "dice throw".
*
* @notes
2021-01-08 23:46:28 +01:00
* Be aware that, even though this behaves like one roll, it actually throws several ones internally
2021-01-08 23:41:23 +01:00
*
* @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`
*/
2021-01-08 23:18:01 +01:00
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)
2021-01-08 23:31:42 +01:00
throw new SyntaxError(game.i18n.localize("DS4.ErrorDiceCritOverlap"));
2021-01-08 23:18:01 +01:00
}
}
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,
2021-01-08 23:18:01 +01:00
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<RollResult>)
.map((r) => {
2021-01-08 23:31:42 +01:00
const intermediateResults = [r];
2021-01-08 23:18:01 +01:00
let checked = 0;
2021-01-08 23:31:42 +01:00
while (checked < intermediateResults.length) {
2021-02-05 02:52:55 +01:00
const r = intermediateResults[checked];
2021-01-08 23:18:01 +01:00
checked++;
if (!r.active) continue;
if (r.dice[0] <= this.maxCritSuccess) {
r.exploded = true;
const newRoll = this.rollWithDifferentBorders({}, true);
2021-01-08 23:31:42 +01:00
intermediateResults.push(newRoll);
2021-01-08 23:18:01 +01:00
}
2021-01-08 23:31:42 +01:00
if (checked > 1000) throw new Error(game.i18n.localize("DS4.ErrorExplodingRecursionLimitExceeded"));
2021-01-08 23:18:01 +01:00
}
2021-01-08 23:31:42 +01:00
return intermediateResults;
2021-01-08 23:18:01 +01:00
})
.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
};
}