Make dice viewable seperately in DS4Check
This commit is contained in:
parent
a542dd1575
commit
9c1d2f081a
8 changed files with 157 additions and 651 deletions
|
@ -1,244 +0,0 @@
|
||||||
import { RollResult, RollResultStatus } from "../../src/module/rolls/roll-data";
|
|
||||||
import { rollCheckMultipleDice, rollCheckSingleDie } from "../../src/module/rolls/roll-executor";
|
|
||||||
|
|
||||||
describe("DS4 Rolls with one die and no modifications.", () => {
|
|
||||||
it("Should do a regular success roll.", () => {
|
|
||||||
expect(rollCheckSingleDie(12, {}, [4])).toEqual(new RollResult(4, RollResultStatus.SUCCESS, [4], true));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should do a single success roll on success upper edge case.", () => {
|
|
||||||
expect(rollCheckSingleDie(4, {}, [4])).toEqual(new RollResult(4, RollResultStatus.SUCCESS, [4], true));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should do a single failure roll on lower edge case.", () => {
|
|
||||||
expect(rollCheckSingleDie(4, {}, [5])).toEqual(new RollResult(0, RollResultStatus.FAILURE, [5], true));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should do a single failure roll on upper edge case '19'.", () => {
|
|
||||||
expect(rollCheckSingleDie(4, {}, [19])).toEqual(new RollResult(0, RollResultStatus.FAILURE, [19]));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should do a single crit success roll on '1'.", () => {
|
|
||||||
expect(rollCheckSingleDie(4, {}, [1])).toEqual(new RollResult(4, RollResultStatus.CRITICAL_SUCCESS, [1], true));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should do a single crit failure roll on '20'.", () => {
|
|
||||||
expect(rollCheckSingleDie(4, {}, [20])).toEqual(new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [20]));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("DS4 Rolls with one die and slaying dice, first throw.", () => {
|
|
||||||
it("Should do a crit success on `1`", () => {
|
|
||||||
expect(rollCheckSingleDie(4, { useSlayingDice: true }, [1])).toEqual(
|
|
||||||
new RollResult(4, RollResultStatus.CRITICAL_SUCCESS, [1]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should do a crit fail on `20`", () => {
|
|
||||||
expect(rollCheckSingleDie(4, { useSlayingDice: true }, [20])).toEqual(
|
|
||||||
new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [20]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("DS4 Rolls with one die and slaying dice, followup throw.", () => {
|
|
||||||
it("Should do a crit success on `1`", () => {
|
|
||||||
expect(rollCheckSingleDie(4, { useSlayingDice: true, slayingDiceRepetition: true }, [1])).toEqual(
|
|
||||||
new RollResult(4, RollResultStatus.CRITICAL_SUCCESS, [1]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should do a regular fail on `20`", () => {
|
|
||||||
expect(rollCheckSingleDie(4, { useSlayingDice: true, slayingDiceRepetition: true }, [20])).toEqual(
|
|
||||||
new RollResult(0, RollResultStatus.FAILURE, [20]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should do a regular success on `20` with a CTN of 20", () => {
|
|
||||||
expect(rollCheckSingleDie(20, { useSlayingDice: true, slayingDiceRepetition: true }, [20])).toEqual(
|
|
||||||
new RollResult(20, RollResultStatus.SUCCESS, [20]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("DS4 Rolls with one die and crit roll modifications.", () => {
|
|
||||||
it("Should do a crit success on `1`.", () => {
|
|
||||||
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFailure: 19 }, [1])).toEqual(
|
|
||||||
new RollResult(4, RollResultStatus.CRITICAL_SUCCESS, [1]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should do a crit success on `maxCritSucc`.", () => {
|
|
||||||
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFailure: 19 }, [2])).toEqual(
|
|
||||||
new RollResult(4, RollResultStatus.CRITICAL_SUCCESS, [2]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should do a success on lower edge case `3`.", () => {
|
|
||||||
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFailure: 19 }, [3])).toEqual(
|
|
||||||
new RollResult(3, RollResultStatus.SUCCESS, [3]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should do a success on upper edge case `18`.", () => {
|
|
||||||
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFailure: 19 }, [18])).toEqual(
|
|
||||||
new RollResult(0, RollResultStatus.FAILURE, [18]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should do a crit fail on `minCritFailure`.", () => {
|
|
||||||
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFailure: 19 }, [19])).toEqual(
|
|
||||||
new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [19]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should do a crit fail on `20`", () => {
|
|
||||||
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFailure: 19 }, [20])).toEqual(
|
|
||||||
new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [20]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("DS4 Rolls with multiple dice and no modifiers.", () => {
|
|
||||||
it("Should do a crit fail on `20` for first roll.", () => {
|
|
||||||
expect(rollCheckMultipleDice(48, {}, [20, 15, 6])).toEqual(
|
|
||||||
new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [20, 15, 6]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should succeed normally with all rolls crit successes.", () => {
|
|
||||||
expect(rollCheckMultipleDice(48, {}, [1, 1, 1])).toEqual(
|
|
||||||
new RollResult(48, RollResultStatus.CRITICAL_SUCCESS, [1, 1, 1]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should succeed with the last roll not being suficient.", () => {
|
|
||||||
expect(rollCheckMultipleDice(48, {}, [15, 15, 15])).toEqual(
|
|
||||||
new RollResult(30, RollResultStatus.SUCCESS, [15, 15, 15]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should succeed with the last roll a crit success.", () => {
|
|
||||||
expect(rollCheckMultipleDice(48, {}, [15, 15, 1])).toEqual(
|
|
||||||
new RollResult(38, RollResultStatus.SUCCESS, [15, 15, 1]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should succeed with the last roll being 20 and one crit success.", () => {
|
|
||||||
expect(rollCheckMultipleDice(48, {}, [15, 1, 20])).toEqual(
|
|
||||||
new RollResult(43, RollResultStatus.SUCCESS, [15, 1, 20]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should properly maximize throw result with all dice success.", () => {
|
|
||||||
expect(rollCheckMultipleDice(46, {}, [15, 4, 12])).toEqual(
|
|
||||||
new RollResult(31, RollResultStatus.SUCCESS, [15, 4, 12]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should properly maximize throw result with one dice a failure.", () => {
|
|
||||||
expect(rollCheckMultipleDice(46, {}, [15, 8, 20])).toEqual(
|
|
||||||
new RollResult(35, RollResultStatus.SUCCESS, [15, 8, 20]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should maximize on 'lowest dice higher than last CTN and crit success thrown'-Edge case, no change required.", () => {
|
|
||||||
expect(rollCheckMultipleDice(46, {}, [15, 1, 8])).toEqual(
|
|
||||||
new RollResult(35, RollResultStatus.SUCCESS, [15, 1, 8]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should maximize on 2-dice 'lowest dice higher than last CTN and crit success thrown'-Edge case, no change required.", () => {
|
|
||||||
expect(rollCheckMultipleDice(24, {}, [1, 8])).toEqual(
|
|
||||||
new RollResult(20, RollResultStatus.CRITICAL_SUCCESS, [1, 8]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should maximize on 2-dice 'lowest dice higher than last CTN and crit success thrown'-Edge case, change required.", () => {
|
|
||||||
expect(rollCheckMultipleDice(38, {}, [1, 19])).toEqual(
|
|
||||||
new RollResult(37, RollResultStatus.CRITICAL_SUCCESS, [1, 19]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should maximize correctly when swapping with more than one crit success", () => {
|
|
||||||
expect(rollCheckMultipleDice(48, {}, [1, 1, 15])).toEqual(
|
|
||||||
new RollResult(43, RollResultStatus.CRITICAL_SUCCESS, [1, 1, 15]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("DS4 Rolls with multiple dice and min/max modifiers.", () => {
|
|
||||||
it("Should do a crit fail on `19` for first roll.", () => {
|
|
||||||
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFailure: 19 }, [19, 15, 6])).toEqual(
|
|
||||||
new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [19, 15, 6]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should succeed with all rolls crit successes (1 and 2).", () => {
|
|
||||||
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFailure: 19 }, [2, 1, 2])).toEqual(
|
|
||||||
new RollResult(48, RollResultStatus.CRITICAL_SUCCESS, [2, 1, 2]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should succeed with the last roll not being sufficient.", () => {
|
|
||||||
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFailure: 19 }, [15, 15, 15])).toEqual(
|
|
||||||
new RollResult(30, RollResultStatus.SUCCESS, [15, 15, 15]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should succeed with the last roll a crit success `2`.", () => {
|
|
||||||
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFailure: 19 }, [15, 15, 2])).toEqual(
|
|
||||||
new RollResult(38, RollResultStatus.SUCCESS, [15, 15, 2]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should succeed with the last roll being `20` and one crit success '2'.", () => {
|
|
||||||
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFailure: 19 }, [15, 2, 20])).toEqual(
|
|
||||||
new RollResult(43, RollResultStatus.SUCCESS, [15, 2, 20]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should succeed with the last roll being `19` and one crit success '2'.", () => {
|
|
||||||
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFailure: 19 }, [15, 2, 19])).toEqual(
|
|
||||||
new RollResult(42, RollResultStatus.SUCCESS, [15, 2, 19]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("DS4 Rolls with multiple dice and fail modifiers.", () => {
|
|
||||||
it("Should do a crit fail on `19` for first roll.", () => {
|
|
||||||
expect(rollCheckMultipleDice(48, { minCritFailure: 19 }, [19, 15, 6])).toEqual(
|
|
||||||
new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [19, 15, 6]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("DS4 Rolls with multiple dice and success modifiers.", () => {
|
|
||||||
it("Should succeed with all rolls crit successes (1 and 2).", () => {
|
|
||||||
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2 }, [2, 1, 2])).toEqual(
|
|
||||||
new RollResult(48, RollResultStatus.CRITICAL_SUCCESS, [2, 1, 2]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("DS4 Rolls with multiple and slaying dice, first throw", () => {
|
|
||||||
it("Should fail with the first roll being a `20`", () => {
|
|
||||||
expect(rollCheckMultipleDice(48, { useSlayingDice: true }, [20, 2, 19])).toEqual(
|
|
||||||
new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [20, 2, 19], true),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should issue a critical success, even with resorting dice", () => {
|
|
||||||
expect(rollCheckMultipleDice(48, { useSlayingDice: true, maxCritSuccess: 2 }, [2, 19, 15])).toEqual(
|
|
||||||
new RollResult(42, RollResultStatus.CRITICAL_SUCCESS, [2, 19, 15]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("DS4 Rolls with multiple and slaying dice, recurrent throw", () => {
|
|
||||||
it("Should regularly succeed with the first roll being a `20`", () => {
|
|
||||||
expect(rollCheckMultipleDice(48, { useSlayingDice: true, slayingDiceRepetition: true }, [20, 2, 19])).toEqual(
|
|
||||||
new RollResult(41, RollResultStatus.SUCCESS, [20, 2, 19]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { isDiceSwapNecessary } from "../../src/module/rolls/roll-utils";
|
|
||||||
|
|
||||||
describe("Utility function testing if dice swap is necessary", () => {
|
|
||||||
it("Should not swap if all dice are crit successes.", () => {
|
|
||||||
expect(isDiceSwapNecessary([[1, 1, 1], []], 9)).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should not swap if no die is crit success.", () => {
|
|
||||||
expect(isDiceSwapNecessary([[], [2, 2, 2]], 9)).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should not swap if all dice are already in use", () => {
|
|
||||||
expect(isDiceSwapNecessary([[1], [9, 8]], 10)).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should not swap if result does not get any better", () => {
|
|
||||||
expect(isDiceSwapNecessary([[1], [8]], 4)).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should swap if result does get better", () => {
|
|
||||||
expect(isDiceSwapNecessary([[1], [19]], 18)).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,3 +1,22 @@
|
||||||
|
export default function evaluateCheck(
|
||||||
|
dice: number[],
|
||||||
|
checkTargetNumber: number,
|
||||||
|
{
|
||||||
|
maximumCoupResult = 1,
|
||||||
|
minimumFumbleResult = 20,
|
||||||
|
canFumble = true,
|
||||||
|
}: { maximumCoupResult?: number; minimumFumbleResult?: number; canFumble?: boolean } = {},
|
||||||
|
): DS4SubCheckResult[] {
|
||||||
|
const diceWithSubChecks = assignSubChecksToDice(dice, checkTargetNumber, {
|
||||||
|
maximumCoupResult: maximumCoupResult,
|
||||||
|
});
|
||||||
|
return evaluateDiceWithSubChecks(diceWithSubChecks, {
|
||||||
|
maximumCoupResult: maximumCoupResult,
|
||||||
|
minimumFumbleResult: minimumFumbleResult,
|
||||||
|
canFumble: canFumble,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function assignSubChecksToDice(
|
export function assignSubChecksToDice(
|
||||||
dice: number[],
|
dice: number[],
|
||||||
checkTargetNumber: number,
|
checkTargetNumber: number,
|
||||||
|
@ -6,8 +25,9 @@ export function assignSubChecksToDice(
|
||||||
}: {
|
}: {
|
||||||
maximumCoupResult?: number;
|
maximumCoupResult?: number;
|
||||||
} = {},
|
} = {},
|
||||||
): { result: number; checkTargetNumber: number }[] {
|
): DieWithSubCheck[] {
|
||||||
const requiredNumberOfDice = Math.ceil(checkTargetNumber / 20);
|
const requiredNumberOfDice = getRequiredNumberOfDice(checkTargetNumber);
|
||||||
|
|
||||||
if (dice.length !== requiredNumberOfDice || requiredNumberOfDice < 1) {
|
if (dice.length !== requiredNumberOfDice || requiredNumberOfDice < 1) {
|
||||||
throw new Error(game.i18n.localize("DS4.ErrorInvalidNumberOfDice")); // TODO: add i18n
|
throw new Error(game.i18n.localize("DS4.ErrorInvalidNumberOfDice")); // TODO: add i18n
|
||||||
}
|
}
|
||||||
|
@ -55,3 +75,41 @@ function shouldUseCoupForLastSubCheck(
|
||||||
dice[indexOfSmallestNonCoup] + checkTargetNumberForLastSubCheck > 20))
|
dice[indexOfSmallestNonCoup] + checkTargetNumberForLastSubCheck > 20))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DieWithSubCheck {
|
||||||
|
result: number;
|
||||||
|
checkTargetNumber: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DS4SubCheckResult extends DieWithSubCheck, DiceTerm.Result {
|
||||||
|
success?: boolean;
|
||||||
|
failure?: boolean;
|
||||||
|
count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateDiceWithSubChecks(
|
||||||
|
results: DieWithSubCheck[],
|
||||||
|
{
|
||||||
|
maximumCoupResult,
|
||||||
|
minimumFumbleResult,
|
||||||
|
canFumble,
|
||||||
|
}: { maximumCoupResult: number; minimumFumbleResult: number; canFumble: boolean },
|
||||||
|
): DS4SubCheckResult[] {
|
||||||
|
return results.map((dieWithSubCheck, index) => {
|
||||||
|
const result: DS4SubCheckResult = {
|
||||||
|
...dieWithSubCheck,
|
||||||
|
active: dieWithSubCheck.result <= dieWithSubCheck.checkTargetNumber,
|
||||||
|
discarded: dieWithSubCheck.result > dieWithSubCheck.checkTargetNumber,
|
||||||
|
};
|
||||||
|
if (result.result <= maximumCoupResult) {
|
||||||
|
result.success = true;
|
||||||
|
result.count = result.checkTargetNumber;
|
||||||
|
}
|
||||||
|
if (index === 0 && canFumble && result.result >= minimumFumbleResult) result.failure = true;
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRequiredNumberOfDice(checkTargetNumber: number): number {
|
||||||
|
return Math.ceil(checkTargetNumber / 20);
|
||||||
|
}
|
||||||
|
|
|
@ -1,136 +1,138 @@
|
||||||
import { RollResult, RollResultStatus } from "./roll-data";
|
import evaluateCheck, { getRequiredNumberOfDice } from "./check-evaluation";
|
||||||
import { ds4roll } from "./roll-executor";
|
|
||||||
|
|
||||||
interface TermData {
|
interface DS4CheckTermData extends DiceTerm.TermData {
|
||||||
number: number;
|
canFumble: boolean;
|
||||||
faces: number;
|
|
||||||
modifiers: Array<string>;
|
|
||||||
options: Record<string, unknown>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements DS4 Checks as an emulated "dice throw".
|
* 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
|
* @example
|
||||||
* - Roll a check against a Check Target Number (CTV) of 18: `/r dsv18`
|
* - Roll a check against a Check Target Number (CTN) of 18: `/r dsv18`
|
||||||
* - Roll a check with multiple dice against a CTN of 34: `/r dsv34`
|
* - 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 `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 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 {
|
export class DS4Check extends DiceTerm {
|
||||||
constructor(termData: Partial<TermData>) {
|
constructor({ modifiers = [], options = {}, canFumble = true }: Partial<DS4CheckTermData> = {}) {
|
||||||
super({
|
super({
|
||||||
number: termData.number,
|
faces: 20,
|
||||||
faces: termData.faces, // should be null
|
modifiers: modifiers,
|
||||||
modifiers: termData.modifiers ?? [],
|
options: options,
|
||||||
options: termData.options ?? {},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Store and parse target value.
|
this.canFumble = canFumble;
|
||||||
|
|
||||||
|
// Parse and store check target number
|
||||||
const targetValueModifier = this.modifiers.filter((m) => m[0] === "v")[0];
|
const targetValueModifier = this.modifiers.filter((m) => m[0] === "v")[0];
|
||||||
const tvRgx = new RegExp("v([0-9]+)?");
|
const tvRgx = new RegExp("v([0-9]+)?");
|
||||||
const tvMatch = targetValueModifier?.match(tvRgx);
|
const tvMatch = targetValueModifier?.match(tvRgx);
|
||||||
if (tvMatch) {
|
if (tvMatch) {
|
||||||
const [parseTargetValue] = tvMatch.slice(1);
|
const [parseTargetValue] = tvMatch.slice(1);
|
||||||
this.targetValue = parseTargetValue ? parseInt(parseTargetValue) : DS4Check.DEFAULT_TARGET_VALUE;
|
this.checkTargetNumber = parseTargetValue
|
||||||
|
? parseInt(parseTargetValue)
|
||||||
|
: DS4Check.DEFAULT_CHECK_TARGET_NUMBER;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store and parse min/max crit
|
this.number = getRequiredNumberOfDice(this.checkTargetNumber);
|
||||||
const critModifier = this.modifiers.filter((m) => m[0] === "c")[0];
|
|
||||||
const cmRgx = new RegExp("c([0-9]+)?,([0-9]+)?");
|
// Parse and store minimumCoupResult and maximumFumbleResult
|
||||||
const cmMatch = critModifier?.match(cmRgx);
|
const coupFumbleModifier = this.modifiers.filter((m) => m[0] === "c")[0];
|
||||||
if (cmMatch) {
|
const cfmRgx = new RegExp("c([0-9]+)?,([0-9]+)?");
|
||||||
const [parseMaxCritSuccess, parseMinCritFailure] = cmMatch.slice(1);
|
const cfmMatch = coupFumbleModifier?.match(cfmRgx);
|
||||||
this.maxCritSuccess = parseMaxCritSuccess
|
if (cfmMatch) {
|
||||||
? parseInt(parseMaxCritSuccess)
|
const [parseMaximumCoupResult, parseMinimumFumbleResult] = cfmMatch.slice(1);
|
||||||
: DS4Check.DEFAULT_MAX_CRIT_SUCCESS;
|
this.maximumCoupResult = parseMaximumCoupResult
|
||||||
this.minCritFailure = parseMinCritFailure
|
? parseInt(parseMaximumCoupResult)
|
||||||
? parseInt(parseMinCritFailure)
|
: DS4Check.DEFAULT_MAXIMUM_COUP_RESULT;
|
||||||
: DS4Check.DEFAULT_MIN_CRIT_FAILURE;
|
this.minimumFumbleResult = parseMinimumFumbleResult
|
||||||
if (this.minCritFailure <= this.maxCritSuccess)
|
? parseInt(parseMinimumFumbleResult)
|
||||||
|
: DS4Check.DEFAULT_MINIMUM_FUMBLE_RESULT;
|
||||||
|
if (this.minimumFumbleResult <= this.maximumCoupResult)
|
||||||
throw new SyntaxError(game.i18n.localize("DS4.ErrorDiceCritOverlap"));
|
throw new SyntaxError(game.i18n.localize("DS4.ErrorDiceCritOverlap"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
success: boolean | null = null;
|
coup: boolean | null = null;
|
||||||
failure: boolean | null = null;
|
fumble: boolean | null = null;
|
||||||
targetValue = DS4Check.DEFAULT_TARGET_VALUE;
|
canFumble: boolean;
|
||||||
minCritFailure = DS4Check.DEFAULT_MIN_CRIT_FAILURE;
|
checkTargetNumber = DS4Check.DEFAULT_CHECK_TARGET_NUMBER;
|
||||||
maxCritSuccess = DS4Check.DEFAULT_MAX_CRIT_SUCCESS;
|
minimumFumbleResult = DS4Check.DEFAULT_MINIMUM_FUMBLE_RESULT;
|
||||||
|
maximumCoupResult = DS4Check.DEFAULT_MAXIMUM_COUP_RESULT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
roll({ minimize = false, maximize = false } = {}): RollResult {
|
get expression(): string {
|
||||||
const rollResult = this.rollWithDifferentBorders({ minimize, maximize });
|
return `ds${this.modifiers.join("")}`;
|
||||||
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;
|
* @override
|
||||||
if (minimize) {
|
*/
|
||||||
return new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [20], true);
|
get total(): number | null {
|
||||||
} else if (maximize) {
|
if (this.fumble) return 0;
|
||||||
const maximizedDice = Array(Math.ceil(targetValueToUse / 20)).fill(1);
|
return super.total;
|
||||||
return new RollResult(targetValueToUse, RollResultStatus.CRITICAL_SUCCESS, maximizedDice, true);
|
|
||||||
} else {
|
|
||||||
return ds4roll(targetValueToUse, {
|
|
||||||
maxCritSuccess: this.maxCritSuccess,
|
|
||||||
minCritFailure: this.minCritFailure,
|
|
||||||
slayingDiceRepetition: slayingDiceRepetition,
|
|
||||||
useSlayingDice: slayingDiceRepetition,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DS4 only allows recursive explosions
|
/**
|
||||||
explode(modifier: string): void {
|
* @override
|
||||||
const rgx = /[xX]/;
|
*/
|
||||||
const match = modifier.match(rgx);
|
roll({ minimize = false, maximize = false } = {}): DiceTerm.Result {
|
||||||
if (!match) return;
|
// 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 });
|
||||||
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;
|
evaluateResults(): void {
|
||||||
static readonly DEFAULT_MAX_CRIT_SUCCESS = 1;
|
const dice = this.results.map((die) => die.result);
|
||||||
static readonly DEFAULT_MIN_CRIT_FAILURE = 20;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// // 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;
|
||||||
static DENOMINATION = "s";
|
static DENOMINATION = "s";
|
||||||
static MODIFIERS = {
|
static MODIFIERS = {
|
||||||
x: "explode",
|
//x: "explode",
|
||||||
c: (): void => undefined, // Modifier is consumed in constructor for crit
|
c: (): void => undefined, // Modifier is consumed in constructor for crit
|
||||||
v: (): void => undefined, // Modifier is consumed in constructor for target value
|
v: "evaluateResults",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
export interface RollOptions {
|
|
||||||
maxCritSuccess: number;
|
|
||||||
minCritFailure: number;
|
|
||||||
useSlayingDice: boolean;
|
|
||||||
slayingDiceRepetition: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DefaultRollOptions implements RollOptions {
|
|
||||||
public maxCritSuccess = 1;
|
|
||||||
public minCritFailure = 20;
|
|
||||||
public useSlayingDice = false;
|
|
||||||
public slayingDiceRepetition = false;
|
|
||||||
|
|
||||||
mergeWith(other: Partial<RollOptions>): RollOptions {
|
|
||||||
return { ...this, ...other };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RollResult {
|
|
||||||
constructor(
|
|
||||||
public result: number,
|
|
||||||
public status: RollResultStatus,
|
|
||||||
public dice: Array<number>,
|
|
||||||
public active: boolean = true,
|
|
||||||
public exploded: boolean = false,
|
|
||||||
) {
|
|
||||||
if (this.status == RollResultStatus.CRITICAL_FAILURE) {
|
|
||||||
this.failure = true;
|
|
||||||
} else if (this.status == RollResultStatus.CRITICAL_SUCCESS) {
|
|
||||||
this.success = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public failure: boolean | void = undefined;
|
|
||||||
public success: boolean | void = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum RollResultStatus {
|
|
||||||
FAILURE = "FAILURE",
|
|
||||||
SUCCESS = "SUCCESS",
|
|
||||||
CRITICAL_FAILURE = "CRITICAL_FAILURE",
|
|
||||||
CRITICAL_SUCCESS = "CRITICAL_SUCCESS",
|
|
||||||
}
|
|
|
@ -1,118 +0,0 @@
|
||||||
import { DefaultRollOptions, RollOptions, RollResult, RollResultStatus } from "./roll-data";
|
|
||||||
import { DS4RollProvider } from "./roll-provider";
|
|
||||||
import { calculateRollResult, isDiceSwapNecessary, isSlayingDiceRepetition, separateCriticalHits } from "./roll-utils";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs a roll against a check target number, e.g. for usage in battle, but not for herbs.
|
|
||||||
* @param checkTargetValue - the final CTN, including all static modifiers.
|
|
||||||
* @param rollOptions - optional, final option override that affect the checks outcome, e.g. different values for crits or whether slaying dice are used.
|
|
||||||
* @param dice - optional, pass already thrown dice that are used instead of rolling new ones.
|
|
||||||
*/
|
|
||||||
export function ds4roll(
|
|
||||||
checkTargetValue: number,
|
|
||||||
rollOptions: Partial<RollOptions> = {},
|
|
||||||
dice: Array<number> = [],
|
|
||||||
): RollResult {
|
|
||||||
if (checkTargetValue <= 20) {
|
|
||||||
return rollCheckSingleDie(checkTargetValue, rollOptions, dice);
|
|
||||||
} else {
|
|
||||||
return rollCheckMultipleDice(checkTargetValue, rollOptions, dice);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs a roll against a single die (CTN less than or equal 20).
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
* This is not intended for direct usage. Use
|
|
||||||
* {@link ds4roll | the function that is not bound to an amount of Dice} instead.
|
|
||||||
*
|
|
||||||
* @param checkTargetValue - The target value to check against.
|
|
||||||
* @param rollOptions - Options that affect the checks outcome, e.g. different values for crits or whether slaying dice are used.
|
|
||||||
* @param dice - optional, pass already thrown dice that are used instead of rolling new ones.
|
|
||||||
*
|
|
||||||
* @returns An object containing detailed information on the roll result.
|
|
||||||
*/
|
|
||||||
export function rollCheckSingleDie(
|
|
||||||
checkTargetValue: number,
|
|
||||||
rollOptions: Partial<RollOptions>,
|
|
||||||
dice: Array<number> = [],
|
|
||||||
): RollResult {
|
|
||||||
const usedOptions = new DefaultRollOptions().mergeWith(rollOptions);
|
|
||||||
|
|
||||||
if (dice.length != 1) {
|
|
||||||
dice = [new DS4RollProvider().getNextRoll()];
|
|
||||||
}
|
|
||||||
const usedDice = dice;
|
|
||||||
const rolledDie = usedDice[0];
|
|
||||||
|
|
||||||
if (rolledDie <= usedOptions.maxCritSuccess) {
|
|
||||||
return new RollResult(checkTargetValue, RollResultStatus.CRITICAL_SUCCESS, usedDice, true);
|
|
||||||
} else if (rolledDie >= usedOptions.minCritFailure && !isSlayingDiceRepetition(usedOptions)) {
|
|
||||||
return new RollResult(0, RollResultStatus.CRITICAL_FAILURE, usedDice, true);
|
|
||||||
} else {
|
|
||||||
if (rolledDie <= checkTargetValue) {
|
|
||||||
return new RollResult(rolledDie, RollResultStatus.SUCCESS, usedDice, true);
|
|
||||||
} else {
|
|
||||||
return new RollResult(0, RollResultStatus.FAILURE, usedDice, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs a roll against a multitude of die (CTN greater than 20).
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
* This is not intended for direct usage. Use
|
|
||||||
* {@link ds4roll | the function that is not bound to an amount of Dice} instead.
|
|
||||||
*
|
|
||||||
* @param targetValue - The target value to check against.
|
|
||||||
* @param rollOptions - Options that affect the checks outcome, e.g. different values for crits or whether slaying dice are used.
|
|
||||||
* @param dice - Optional array of dice values to consider instead of rolling new ones.
|
|
||||||
*
|
|
||||||
* @returns An object containing detailed information on the roll result.
|
|
||||||
*/
|
|
||||||
export function rollCheckMultipleDice(
|
|
||||||
targetValue: number,
|
|
||||||
rollOptions: Partial<RollOptions>,
|
|
||||||
dice: Array<number> = [],
|
|
||||||
): RollResult {
|
|
||||||
const usedOptions = new DefaultRollOptions().mergeWith(rollOptions);
|
|
||||||
const remainderTargetValue = targetValue % 20;
|
|
||||||
const numberOfDice = Math.ceil(targetValue / 20);
|
|
||||||
|
|
||||||
if (dice.length != numberOfDice) {
|
|
||||||
dice = new DS4RollProvider().getNextRolls(numberOfDice);
|
|
||||||
}
|
|
||||||
const usedDice = dice;
|
|
||||||
|
|
||||||
const firstResult = usedDice[0];
|
|
||||||
const slayingDiceRepetition = isSlayingDiceRepetition(usedOptions);
|
|
||||||
|
|
||||||
// Slaying Dice require a different handling.
|
|
||||||
if (firstResult >= usedOptions.minCritFailure && !slayingDiceRepetition) {
|
|
||||||
return new RollResult(0, RollResultStatus.CRITICAL_FAILURE, usedDice, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [critSuccesses, otherRolls] = separateCriticalHits(usedDice, usedOptions);
|
|
||||||
|
|
||||||
const swapLastWithCrit: boolean = isDiceSwapNecessary([critSuccesses, otherRolls], remainderTargetValue);
|
|
||||||
|
|
||||||
let sortedRollResults: Array<number>;
|
|
||||||
|
|
||||||
if (swapLastWithCrit) {
|
|
||||||
const diceToMove = critSuccesses[0];
|
|
||||||
const remainingSuccesses = critSuccesses.slice(1);
|
|
||||||
sortedRollResults = remainingSuccesses.concat(otherRolls).concat([diceToMove]);
|
|
||||||
} else {
|
|
||||||
sortedRollResults = critSuccesses.concat(otherRolls);
|
|
||||||
}
|
|
||||||
|
|
||||||
const evaluationResult = calculateRollResult(sortedRollResults, remainderTargetValue, usedOptions);
|
|
||||||
|
|
||||||
if (firstResult <= usedOptions.maxCritSuccess) {
|
|
||||||
return new RollResult(evaluationResult, RollResultStatus.CRITICAL_SUCCESS, usedDice, true);
|
|
||||||
} else {
|
|
||||||
return new RollResult(evaluationResult, RollResultStatus.SUCCESS, usedDice, true);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
/**
|
|
||||||
* Runtime-implementation of the {@link RollProvider}.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* Do not use for tests, it will inevitably fail because the `Roll` class is only provided from declarations, not as implementation!
|
|
||||||
*/
|
|
||||||
export class DS4RollProvider implements RollProvider {
|
|
||||||
getNextRoll(): number {
|
|
||||||
const rand = CONFIG.Dice.randomUniform();
|
|
||||||
return Math.ceil(rand * 20);
|
|
||||||
}
|
|
||||||
|
|
||||||
getNextRolls(amount: number): Array<number> {
|
|
||||||
return Array(amount)
|
|
||||||
.fill(0)
|
|
||||||
.map(() => this.getNextRoll());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides methods to fetch one or multiple rolls.
|
|
||||||
*/
|
|
||||||
export interface RollProvider {
|
|
||||||
getNextRoll(): number;
|
|
||||||
getNextRolls(amount: number): Array<number>;
|
|
||||||
}
|
|
|
@ -1,100 +0,0 @@
|
||||||
import { partition, zip } from "../common/utils";
|
|
||||||
import { RollOptions } from "./roll-data";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Separates critical hits ("Coups") from throws, that get counted with their regular value.
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @private_remarks
|
|
||||||
* This uses an internal implementation of a `partition` method. Don't let typescript fool you, it will tell you that a partition method is available for Arrays, but that one's imported globally from foundry's declarations and not available during the test stage!
|
|
||||||
*
|
|
||||||
* @param dice - The dice values.
|
|
||||||
* @param usedOptions - Options that affect the check's behavior.
|
|
||||||
* @returns A tuple containing two arrays of dice values, the first one containing all critical hits, the second one containing all others. Both arrays are sorted descending by value.
|
|
||||||
*/
|
|
||||||
export function separateCriticalHits(dice: Array<number>, usedOptions: RollOptions): CritsAndNonCrits {
|
|
||||||
const [critSuccesses, otherRolls] = partition(dice, (v: number) => {
|
|
||||||
return v <= usedOptions.maxCritSuccess;
|
|
||||||
}).map((a) => a.sort((r1, r2) => r2 - r1));
|
|
||||||
|
|
||||||
return [critSuccesses, otherRolls];
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Helper type to properly bind combinations of critical and non critical dice.
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
type CritsAndNonCrits = [Array<number>, Array<number>];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates if a critical success should be moved to the last position in order to maximize the check's result.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* With regular dice rolling rules and a check target number of 31, the two dice 1 and 19 can get to a check result of 30.
|
|
||||||
* This method would be called as follows:
|
|
||||||
* ```ts
|
|
||||||
* isDiceSwapNecessary([[1], [19]], 11)
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @param critsAndNonCrits - The dice values thrown. It is assumed that both critical successes and other rolls are sorted descending.
|
|
||||||
* @param remainingTargetValue - The target value for the last dice, that is the only one that can be less than 20.
|
|
||||||
* @returns Bool indicating whether a critical success has to be used as the last dice.
|
|
||||||
*/
|
|
||||||
export function isDiceSwapNecessary(
|
|
||||||
[critSuccesses, otherRolls]: CritsAndNonCrits,
|
|
||||||
remainingTargetValue: number,
|
|
||||||
): boolean {
|
|
||||||
if (critSuccesses.length == 0 || otherRolls.length == 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const amountOfOtherRolls = otherRolls.length;
|
|
||||||
const lastDice = otherRolls[amountOfOtherRolls - 1];
|
|
||||||
if (lastDice <= remainingTargetValue) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return lastDice + remainingTargetValue > 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the options indicate that the current check is emerging from a crit success on a roll with slaying dice.
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @param opts - The roll options to check against
|
|
||||||
*/
|
|
||||||
export function isSlayingDiceRepetition(opts: RollOptions): boolean {
|
|
||||||
return opts.useSlayingDice && opts.slayingDiceRepetition;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate the check value of an array of dice, assuming the dice should be used in order of occurence.
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @param assignedRollResults - The dice values in the order of usage.
|
|
||||||
* @param remainderTargetValue - Target value for the last dice (the only one differing from `20`).
|
|
||||||
* @param rollOptions - Config object containing options that change the way dice results are handled.
|
|
||||||
*
|
|
||||||
* @returns {number} The total check value.
|
|
||||||
*/
|
|
||||||
export function calculateRollResult(
|
|
||||||
assignedRollResults: Array<number>,
|
|
||||||
remainderTargetValue: number,
|
|
||||||
rollOptions: RollOptions,
|
|
||||||
): number {
|
|
||||||
const numberOfDice = assignedRollResults.length;
|
|
||||||
|
|
||||||
const maxResultPerDie: Array<number> = Array(numberOfDice).fill(20);
|
|
||||||
maxResultPerDie[numberOfDice - 1] = remainderTargetValue;
|
|
||||||
|
|
||||||
const rollsAndMaxValues = zip(assignedRollResults, maxResultPerDie);
|
|
||||||
|
|
||||||
return rollsAndMaxValues
|
|
||||||
.map(([v, m]) => {
|
|
||||||
return v <= rollOptions.maxCritSuccess ? [m, m] : [v, m];
|
|
||||||
})
|
|
||||||
.filter(([v, m]) => v <= m)
|
|
||||||
.map(([v]) => v)
|
|
||||||
.reduce((a, b) => a + b);
|
|
||||||
}
|
|
Loading…
Reference in a new issue