135 lines
5 KiB
TypeScript
135 lines
5 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>;
|
||
|
}
|
||
|
|
||
|
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 ?? {},
|
||
|
});
|
||
|
console.log("This @ constructor: ", termData);
|
||
|
|
||
|
// 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("There's an overlap between Fumbles and Coups");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
success = null;
|
||
|
failure = null;
|
||
|
targetValue = DS4Check.DEFAULT_TARGET_VALUE;
|
||
|
minCritFailure = DS4Check.DEFAULT_MIN_CRIT_FAILURE;
|
||
|
maxCritSuccess = DS4Check.DEFAULT_MAX_CRIT_SUCCESS;
|
||
|
|
||
|
/**
|
||
|
* @override
|
||
|
* @param param0
|
||
|
*
|
||
|
* @private_notes This gets only called once for the first roll.
|
||
|
*/
|
||
|
roll({ minimize = false, maximize = false } = {}): RollResult {
|
||
|
console.log(`This targetValue @ roll: ${this.targetValue}`);
|
||
|
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,
|
||
|
minCritFail: 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<RollResult>)
|
||
|
.map((r) => {
|
||
|
const intermedResult = [r];
|
||
|
|
||
|
let checked = 0;
|
||
|
while (checked < intermedResult.length) {
|
||
|
const r = (intermedResult as Array<RollResult>)[checked];
|
||
|
checked++;
|
||
|
if (!r.active) continue;
|
||
|
|
||
|
if (r.dice[0] <= this.maxCritSuccess) {
|
||
|
r.exploded = true;
|
||
|
const newRoll = this.rollWithDifferentBorders({}, true);
|
||
|
intermedResult.push(newRoll);
|
||
|
}
|
||
|
|
||
|
if (checked > 1000) throw new Error("Maximum recursion depth for exploding dice roll exceeded");
|
||
|
}
|
||
|
return intermedResult;
|
||
|
})
|
||
|
.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;
|
||
|
// TODO: add to Type declarations
|
||
|
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
|
||
|
};
|
||
|
}
|