Merge branch '008-chatRollInterface' into 'master'
Resolve "Basic Roll interface" Closes #8 See merge request dungeonslayers/ds4!29
This commit is contained in:
commit
70d183dee5
8 changed files with 271 additions and 191 deletions
|
@ -1,76 +1,43 @@
|
|||
import { rollCheckMultipleDice, rollCheckSingleDie } from "../../../src/module/rolls/roll-executor";
|
||||
import { RollProvider } from "../../../src/module/rolls/roll-provider";
|
||||
|
||||
import "jasmine";
|
||||
import { RollResult, RollResultStatus } from "../../../src/module/rolls/roll-data";
|
||||
|
||||
function mockSingleThrow(value: number): RollProvider {
|
||||
const rollProvider = jasmine.createSpyObj("rollProvider", ["getNextRoll"]);
|
||||
rollProvider.getNextRoll = jasmine.createSpy("getNextRoll").and.returnValue(value);
|
||||
return rollProvider;
|
||||
}
|
||||
|
||||
function mockMultipleThrows(values: Array<number>): RollProvider {
|
||||
const rollProvider = jasmine.createSpyObj("rollProvider", ["getNextRolls"]);
|
||||
rollProvider.getNextRolls = jasmine.createSpy("getNextRolls").and.returnValue(values);
|
||||
return rollProvider;
|
||||
}
|
||||
|
||||
describe("DS4 Rolls with one die and no modifications.", () => {
|
||||
it("Should do a regular success roll.", () => {
|
||||
const rollProvider = mockSingleThrow(4);
|
||||
|
||||
expect(rollCheckSingleDie(12, {}, rollProvider)).toEqual(new RollResult(4, RollResultStatus.SUCCESS, [4]));
|
||||
expect(rollCheckSingleDie(12, {}, [4])).toEqual(new RollResult(4, RollResultStatus.SUCCESS, [4], true));
|
||||
});
|
||||
|
||||
it("Should do a single success roll on success upper edge case.", () => {
|
||||
const rollProvider = mockSingleThrow(4);
|
||||
|
||||
expect(rollCheckSingleDie(4, {}, rollProvider)).toEqual(new RollResult(4, RollResultStatus.SUCCESS, [4]));
|
||||
expect(rollCheckSingleDie(4, {}, [4])).toEqual(new RollResult(4, RollResultStatus.SUCCESS, [4], true));
|
||||
});
|
||||
|
||||
it("Should do a single failure roll on lower edge case.", () => {
|
||||
const rollProvider = mockSingleThrow(5);
|
||||
|
||||
expect(rollCheckSingleDie(4, {}, rollProvider)).toEqual(new RollResult(0, RollResultStatus.FAILURE, [5]));
|
||||
expect(rollCheckSingleDie(4, {}, [5])).toEqual(new RollResult(0, RollResultStatus.FAILURE, [5], true));
|
||||
});
|
||||
|
||||
it("Should do a single failure roll on upper edge case '19'.", () => {
|
||||
const rollProvider = mockSingleThrow(19);
|
||||
|
||||
expect(rollCheckSingleDie(4, {}, rollProvider)).toEqual(new RollResult(0, RollResultStatus.FAILURE, [19]));
|
||||
expect(rollCheckSingleDie(4, {}, [19])).toEqual(new RollResult(0, RollResultStatus.FAILURE, [19]));
|
||||
});
|
||||
|
||||
it("Should do a single crit success roll on '1'.", () => {
|
||||
const rollProvider = mockSingleThrow(1);
|
||||
|
||||
expect(rollCheckSingleDie(4, {}, rollProvider)).toEqual(
|
||||
new RollResult(4, RollResultStatus.CRITICAL_SUCCESS, [1]),
|
||||
);
|
||||
expect(rollCheckSingleDie(4, {}, [1])).toEqual(new RollResult(4, RollResultStatus.CRITICAL_SUCCESS, [1], true));
|
||||
});
|
||||
|
||||
it("Should do a single crit failure roll on '20'.", () => {
|
||||
const rollProvider = mockSingleThrow(20);
|
||||
|
||||
expect(rollCheckSingleDie(4, {}, rollProvider)).toEqual(
|
||||
new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [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`", () => {
|
||||
const rollProvider = mockSingleThrow(1);
|
||||
|
||||
expect(rollCheckSingleDie(4, { useSlayingDice: true }, rollProvider)).toEqual(
|
||||
expect(rollCheckSingleDie(4, { useSlayingDice: true }, [1])).toEqual(
|
||||
new RollResult(4, RollResultStatus.CRITICAL_SUCCESS, [1]),
|
||||
);
|
||||
});
|
||||
|
||||
it("Should do a crit fail on `20`", () => {
|
||||
const rollProvider = mockSingleThrow(20);
|
||||
|
||||
expect(rollCheckSingleDie(4, { useSlayingDice: true }, rollProvider)).toEqual(
|
||||
expect(rollCheckSingleDie(4, { useSlayingDice: true }, [20])).toEqual(
|
||||
new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [20]),
|
||||
);
|
||||
});
|
||||
|
@ -78,25 +45,19 @@ describe("DS4 Rolls with one die and slaying dice, first throw.", () => {
|
|||
|
||||
describe("DS4 Rolls with one die and slaying dice, followup throw.", () => {
|
||||
it("Should do a crit success on `1`", () => {
|
||||
const rollProvider = mockSingleThrow(1);
|
||||
|
||||
expect(rollCheckSingleDie(4, { useSlayingDice: true, slayingDiceRepetition: true }, rollProvider)).toEqual(
|
||||
expect(rollCheckSingleDie(4, { useSlayingDice: true, slayingDiceRepetition: true }, [1])).toEqual(
|
||||
new RollResult(4, RollResultStatus.CRITICAL_SUCCESS, [1]),
|
||||
);
|
||||
});
|
||||
|
||||
it("Should do a regular fail on `20`", () => {
|
||||
const rollProvider = mockSingleThrow(20);
|
||||
|
||||
expect(rollCheckSingleDie(4, { useSlayingDice: true, slayingDiceRepetition: true }, rollProvider)).toEqual(
|
||||
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", () => {
|
||||
const rollProvider = mockSingleThrow(20);
|
||||
|
||||
expect(rollCheckSingleDie(20, { useSlayingDice: true, slayingDiceRepetition: true }, rollProvider)).toEqual(
|
||||
expect(rollCheckSingleDie(20, { useSlayingDice: true, slayingDiceRepetition: true }, [20])).toEqual(
|
||||
new RollResult(20, RollResultStatus.SUCCESS, [20]),
|
||||
);
|
||||
});
|
||||
|
@ -104,49 +65,37 @@ describe("DS4 Rolls with one die and slaying dice, followup throw.", () => {
|
|||
|
||||
describe("DS4 Rolls with one die and crit roll modifications.", () => {
|
||||
it("Should do a crit success on `1`.", () => {
|
||||
const rollProvider = mockSingleThrow(1);
|
||||
|
||||
expect(rollCheckSingleDie(4, { maxCritSucc: 2, minCritFail: 19 }, rollProvider)).toEqual(
|
||||
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFail: 19 }, [1])).toEqual(
|
||||
new RollResult(4, RollResultStatus.CRITICAL_SUCCESS, [1]),
|
||||
);
|
||||
});
|
||||
|
||||
it("Should do a crit success on `maxCritSucc`.", () => {
|
||||
const rollProvider = mockSingleThrow(2);
|
||||
|
||||
expect(rollCheckSingleDie(4, { maxCritSucc: 2, minCritFail: 19 }, rollProvider)).toEqual(
|
||||
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFail: 19 }, [2])).toEqual(
|
||||
new RollResult(4, RollResultStatus.CRITICAL_SUCCESS, [2]),
|
||||
);
|
||||
});
|
||||
|
||||
it("Should do a success on lower edge case `3`.", () => {
|
||||
const rollProvider = mockSingleThrow(3);
|
||||
|
||||
expect(rollCheckSingleDie(4, { maxCritSucc: 2, minCritFail: 19 }, rollProvider)).toEqual(
|
||||
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFail: 19 }, [3])).toEqual(
|
||||
new RollResult(3, RollResultStatus.SUCCESS, [3]),
|
||||
);
|
||||
});
|
||||
|
||||
it("Should do a success on upper edge case `18`.", () => {
|
||||
const rollProvider = mockSingleThrow(18);
|
||||
|
||||
expect(rollCheckSingleDie(4, { maxCritSucc: 2, minCritFail: 19 }, rollProvider)).toEqual(
|
||||
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFail: 19 }, [18])).toEqual(
|
||||
new RollResult(0, RollResultStatus.FAILURE, [18]),
|
||||
);
|
||||
});
|
||||
|
||||
it("Should do a crit fail on `minCritFail`.", () => {
|
||||
const rollProvider = mockSingleThrow(19);
|
||||
|
||||
expect(rollCheckSingleDie(4, { maxCritSucc: 2, minCritFail: 19 }, rollProvider)).toEqual(
|
||||
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFail: 19 }, [19])).toEqual(
|
||||
new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [19]),
|
||||
);
|
||||
});
|
||||
|
||||
it("Should do a crit fail on `20`", () => {
|
||||
const rollProvider = mockSingleThrow(20);
|
||||
|
||||
expect(rollCheckSingleDie(4, { maxCritSucc: 2, minCritFail: 19 }, rollProvider)).toEqual(
|
||||
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFail: 19 }, [20])).toEqual(
|
||||
new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [20]),
|
||||
);
|
||||
});
|
||||
|
@ -154,149 +103,113 @@ describe("DS4 Rolls with one die and crit roll modifications.", () => {
|
|||
|
||||
describe("DS4 Rolls with multiple dice and no modifiers.", () => {
|
||||
it("Should do a crit fail on `20` for first roll.", () => {
|
||||
const rollProvider = mockMultipleThrows([20, 15, 6]);
|
||||
|
||||
expect(rollCheckMultipleDice(48, {}, rollProvider)).toEqual(
|
||||
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.", () => {
|
||||
const rollProvider = mockMultipleThrows([1, 1, 1]);
|
||||
|
||||
expect(rollCheckMultipleDice(48, {}, rollProvider)).toEqual(
|
||||
new RollResult(48, RollResultStatus.SUCCESS, [1, 1, 1]),
|
||||
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.", () => {
|
||||
const rollProvider = mockMultipleThrows([15, 15, 15]);
|
||||
|
||||
expect(rollCheckMultipleDice(48, {}, rollProvider)).toEqual(
|
||||
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.", () => {
|
||||
const rollProvider = mockMultipleThrows([15, 15, 1]);
|
||||
|
||||
expect(rollCheckMultipleDice(48, {}, rollProvider)).toEqual(
|
||||
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.", () => {
|
||||
const rollProvider = mockMultipleThrows([15, 1, 20]);
|
||||
|
||||
expect(rollCheckMultipleDice(48, {}, rollProvider)).toEqual(
|
||||
new RollResult(43, RollResultStatus.SUCCESS, [20, 15, 1]),
|
||||
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.", () => {
|
||||
const rollProvider = mockMultipleThrows([15, 4, 12]);
|
||||
|
||||
expect(rollCheckMultipleDice(46, {}, rollProvider)).toEqual(
|
||||
new RollResult(31, RollResultStatus.SUCCESS, [15, 12, 4]),
|
||||
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.", () => {
|
||||
const rollProvider = mockMultipleThrows([15, 8, 20]);
|
||||
|
||||
expect(rollCheckMultipleDice(46, {}, rollProvider)).toEqual(
|
||||
new RollResult(35, RollResultStatus.SUCCESS, [20, 15, 8]),
|
||||
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.", () => {
|
||||
const rollProvider = mockMultipleThrows([15, 1, 8]);
|
||||
|
||||
expect(rollCheckMultipleDice(46, {}, rollProvider)).toEqual(
|
||||
new RollResult(35, RollResultStatus.SUCCESS, [1, 15, 8]),
|
||||
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.", () => {
|
||||
const rollProvider = mockMultipleThrows([1, 8]);
|
||||
|
||||
expect(rollCheckMultipleDice(24, {}, rollProvider)).toEqual(
|
||||
new RollResult(20, RollResultStatus.SUCCESS, [1, 8]),
|
||||
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.", () => {
|
||||
const rollProvider = mockMultipleThrows([1, 19]);
|
||||
|
||||
expect(rollCheckMultipleDice(38, {}, rollProvider)).toEqual(
|
||||
new RollResult(37, RollResultStatus.SUCCESS, [19, 1]),
|
||||
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", () => {
|
||||
const rollProvider = mockMultipleThrows([1, 1, 15]);
|
||||
|
||||
expect(rollCheckMultipleDice(48, {}, rollProvider)).toEqual(
|
||||
new RollResult(43, RollResultStatus.SUCCESS, [1, 15, 1]),
|
||||
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.", () => {
|
||||
const rollProvider = mockMultipleThrows([19, 15, 6]);
|
||||
|
||||
expect(rollCheckMultipleDice(48, { maxCritSucc: 2, minCritFail: 19 }, rollProvider)).toEqual(
|
||||
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFail: 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).", () => {
|
||||
const rollProvider = mockMultipleThrows([2, 1, 2]);
|
||||
|
||||
expect(rollCheckMultipleDice(48, { maxCritSucc: 2, minCritFail: 19 }, rollProvider)).toEqual(
|
||||
new RollResult(48, RollResultStatus.SUCCESS, [2, 2, 1]),
|
||||
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFail: 19 }, [2, 1, 2])).toEqual(
|
||||
new RollResult(48, RollResultStatus.CRITICAL_SUCCESS, [2, 1, 2]),
|
||||
);
|
||||
});
|
||||
|
||||
it("Should succeed with the last roll not being sufficient.", () => {
|
||||
const rollProvider = mockMultipleThrows([15, 15, 15]);
|
||||
|
||||
expect(rollCheckMultipleDice(48, { maxCritSucc: 2, minCritFail: 19 }, rollProvider)).toEqual(
|
||||
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFail: 19 }, [15, 15, 15])).toEqual(
|
||||
new RollResult(30, RollResultStatus.SUCCESS, [15, 15, 15]),
|
||||
);
|
||||
});
|
||||
|
||||
it("Should succeed with the last roll a crit success `2`.", () => {
|
||||
const rollProvider = mockMultipleThrows([15, 15, 2]);
|
||||
|
||||
expect(rollCheckMultipleDice(48, { maxCritSucc: 2, minCritFail: 19 }, rollProvider)).toEqual(
|
||||
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFail: 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'.", () => {
|
||||
const rollProvider = mockMultipleThrows([15, 2, 20]);
|
||||
|
||||
expect(rollCheckMultipleDice(48, { maxCritSucc: 2, minCritFail: 19 }, rollProvider)).toEqual(
|
||||
new RollResult(43, RollResultStatus.SUCCESS, [20, 15, 2]),
|
||||
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFail: 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'.", () => {
|
||||
const rollProvider = mockMultipleThrows([15, 2, 19]);
|
||||
|
||||
expect(rollCheckMultipleDice(48, { maxCritSucc: 2, minCritFail: 19 }, rollProvider)).toEqual(
|
||||
new RollResult(42, RollResultStatus.SUCCESS, [19, 15, 2]),
|
||||
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFail: 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.", () => {
|
||||
const rollProvider = mockMultipleThrows([19, 15, 6]);
|
||||
|
||||
expect(rollCheckMultipleDice(48, { minCritFail: 19 }, rollProvider)).toEqual(
|
||||
expect(rollCheckMultipleDice(48, { minCritFail: 19 }, [19, 15, 6])).toEqual(
|
||||
new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [19, 15, 6]),
|
||||
);
|
||||
});
|
||||
|
@ -304,38 +217,30 @@ describe("DS4 Rolls with multiple dice and fail modifiers.", () => {
|
|||
|
||||
describe("DS4 Rolls with multiple dice and success modifiers.", () => {
|
||||
it("Should succeed with all rolls crit successes (1 and 2).", () => {
|
||||
const rollProvider = mockMultipleThrows([2, 1, 2]);
|
||||
|
||||
expect(rollCheckMultipleDice(48, { maxCritSucc: 2 }, rollProvider)).toEqual(
|
||||
new RollResult(48, RollResultStatus.SUCCESS, [2, 2, 1]),
|
||||
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`", () => {
|
||||
const rollProvider = mockMultipleThrows([20, 2, 19]);
|
||||
|
||||
expect(rollCheckMultipleDice(48, { useSlayingDice: true }, rollProvider)).toEqual(
|
||||
new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [20, 2, 19]),
|
||||
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", () => {
|
||||
const rollProvider = mockMultipleThrows([2, 19, 15]);
|
||||
|
||||
expect(rollCheckMultipleDice(48, { useSlayingDice: true, maxCritSucc: 2 }, rollProvider)).toEqual(
|
||||
new RollResult(42, RollResultStatus.CRITICAL_SUCCESS, [19, 15, 2]),
|
||||
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`", () => {
|
||||
const rollProvider = mockMultipleThrows([20, 2, 19]);
|
||||
|
||||
expect(rollCheckMultipleDice(48, { useSlayingDice: true, slayingDiceRepetition: true }, rollProvider)).toEqual(
|
||||
new RollResult(41, RollResultStatus.SUCCESS, [20, 19, 2]),
|
||||
expect(rollCheckMultipleDice(48, { useSlayingDice: true, slayingDiceRepetition: true }, [20, 2, 19])).toEqual(
|
||||
new RollResult(41, RollResultStatus.SUCCESS, [20, 2, 19]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -110,5 +110,7 @@
|
|||
"DS4.ProfileWeight": "Weight",
|
||||
"DS4.ProfileEyeColor": "Eye Color",
|
||||
"DS4.ProfileSpecialCharacteristics": "Special Characteristics",
|
||||
"DS4.WarningManageActiveEffectOnOwnedItem": "Managing Active Effects within an Owned Item is not currently supported and will be added in a subsequent update."
|
||||
"DS4.WarningManageActiveEffectOnOwnedItem": "Managing Active Effects within an Owned Item is not currently supported and will be added in a subsequent update.",
|
||||
"DS4.ErrorDiceCritOverlap": "There's an overlap between Fumbles and Coups",
|
||||
"DS4.ErrorExplodingRecursionLimitExceeded": "Maximum recursion depth for exploding dice roll exceeded"
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { DS4ActorSheet } from "./actor/actor-sheet";
|
|||
import { DS4Item } from "./item/item";
|
||||
import { DS4ItemSheet } from "./item/item-sheet";
|
||||
import { DS4 } from "./config";
|
||||
import { DS4Check } from "./rolls/check";
|
||||
|
||||
Hooks.once("init", async function () {
|
||||
console.log(`DS4 | Initializing the DS4 Game System\n${DS4.ASCII}`);
|
||||
|
@ -21,6 +22,14 @@ Hooks.once("init", async function () {
|
|||
CONFIG.Actor.entityClass = DS4Actor as typeof Actor;
|
||||
CONFIG.Item.entityClass = DS4Item as typeof Item;
|
||||
|
||||
// Configure Dice
|
||||
CONFIG.Dice.types = [Die, DS4Check];
|
||||
CONFIG.Dice.terms = {
|
||||
c: Coin,
|
||||
d: Die,
|
||||
s: DS4Check,
|
||||
};
|
||||
|
||||
// Register sheet application classes
|
||||
Actors.unregisterSheet("core", ActorSheet);
|
||||
Actors.registerSheet("ds4", DS4ActorSheet, { makeDefault: true });
|
||||
|
|
142
src/module/rolls/check.ts
Normal file
142
src/module/rolls/check.ts
Normal file
|
@ -0,0 +1,142 @@
|
|||
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 = 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,
|
||||
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 intermediateResults = [r];
|
||||
|
||||
let checked = 0;
|
||||
while (checked < intermediateResults.length) {
|
||||
const r = (intermediateResults as Array<RollResult>)[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;
|
||||
// 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
|
||||
};
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
export interface RollOptions {
|
||||
maxCritSucc: number;
|
||||
maxCritSuccess: number;
|
||||
minCritFail: number;
|
||||
useSlayingDice: boolean;
|
||||
slayingDiceRepetition: boolean;
|
||||
}
|
||||
|
||||
export class DefaultRollOptions implements RollOptions {
|
||||
public maxCritSucc = 1;
|
||||
public maxCritSuccess = 1;
|
||||
public minCritFail = 20;
|
||||
public useSlayingDice = false;
|
||||
public slayingDiceRepetition = false;
|
||||
|
@ -17,12 +17,27 @@ export class DefaultRollOptions implements RollOptions {
|
|||
}
|
||||
|
||||
export class RollResult {
|
||||
constructor(public value: number, public status: RollResultStatus, public dice: Array<number>) {}
|
||||
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,
|
||||
SUCCESS,
|
||||
CRITICAL_FAILURE,
|
||||
CRITICAL_SUCCESS,
|
||||
FAILURE = "FAILURE",
|
||||
SUCCESS = "SUCCESS",
|
||||
CRITICAL_FAILURE = "CRITICAL_FAILURE",
|
||||
CRITICAL_SUCCESS = "CRITICAL_SUCCESS",
|
||||
}
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
import { DefaultRollOptions, RollOptions, RollResult, RollResultStatus } from "./roll-data";
|
||||
import { DS4RollProvider, RollProvider } from "./roll-provider";
|
||||
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 {number} checkTargetValue the final CTN, including all static modifiers.
|
||||
* @param {Partial<RollOptions>} rollOptions optional, final option override that affect the checks outcome, e.g. different values for crits or whether slaying dice are used.
|
||||
* @param {Array<number>} dice optional, pass already thrown dice that are used instead of rolling new ones.
|
||||
*/
|
||||
export function ds4roll(checkTargetValue: number, rollOptions: Partial<RollOptions> = {}): RollResult {
|
||||
export function ds4roll(
|
||||
checkTargetValue: number,
|
||||
rollOptions: Partial<RollOptions> = {},
|
||||
dice: Array<number> = null,
|
||||
): RollResult {
|
||||
if (checkTargetValue <= 20) {
|
||||
return rollCheckSingleDie(checkTargetValue, rollOptions);
|
||||
return rollCheckSingleDie(checkTargetValue, rollOptions, dice);
|
||||
} else {
|
||||
return rollCheckMultipleDice(checkTargetValue, rollOptions);
|
||||
return rollCheckMultipleDice(checkTargetValue, rollOptions, dice);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,33 +27,34 @@ export function ds4roll(checkTargetValue: number, rollOptions: Partial<RollOptio
|
|||
* This is not intended for direct usage. Use
|
||||
* {@link ds4roll | the function that is not bound to an amount of Dice} instead.
|
||||
*
|
||||
* @remarks
|
||||
* The `provider` is only exposed for testing.
|
||||
*
|
||||
* @param {number} checkTargetValue - The target value to check against.
|
||||
* @param {RollOptions} rollOptions - Options that affect the checks outcome, e.g. different values for crits or whether slaying dice are used.
|
||||
* @param {RollProvider} provider - Service providing the various, real dice throws.
|
||||
* @param {Array<number>} dice optional, pass already thrown dice that are used instead of rolling new ones.
|
||||
*
|
||||
* @returns {RollResult} An object containing detailed information on the roll result.
|
||||
*/
|
||||
export function rollCheckSingleDie(
|
||||
checkTargetValue: number,
|
||||
rollOptions: Partial<RollOptions>,
|
||||
provider: RollProvider = new DS4RollProvider(),
|
||||
dice: Array<number> = null,
|
||||
): RollResult {
|
||||
const usedOptions = new DefaultRollOptions().mergeWith(rollOptions);
|
||||
const roll = provider.getNextRoll();
|
||||
const dice = [roll];
|
||||
|
||||
if (roll <= usedOptions.maxCritSucc) {
|
||||
return new RollResult(checkTargetValue, RollResultStatus.CRITICAL_SUCCESS, dice);
|
||||
} else if (roll >= usedOptions.minCritFail && !isSlayingDiceRepetition(usedOptions)) {
|
||||
return new RollResult(0, RollResultStatus.CRITICAL_FAILURE, dice);
|
||||
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.minCritFail && !isSlayingDiceRepetition(usedOptions)) {
|
||||
return new RollResult(0, RollResultStatus.CRITICAL_FAILURE, usedDice, true);
|
||||
} else {
|
||||
if (roll <= checkTargetValue) {
|
||||
return new RollResult(roll, RollResultStatus.SUCCESS, dice);
|
||||
if (rolledDie <= checkTargetValue) {
|
||||
return new RollResult(rolledDie, RollResultStatus.SUCCESS, usedDice, true);
|
||||
} else {
|
||||
return new RollResult(0, RollResultStatus.FAILURE, dice);
|
||||
return new RollResult(0, RollResultStatus.FAILURE, usedDice, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,35 +66,35 @@ export function rollCheckSingleDie(
|
|||
* This is not intended for direct usage. Use
|
||||
* {@link ds4roll | the function that is not bound to an amount of Dice} instead.
|
||||
*
|
||||
* @remarks
|
||||
* The `provider` is only exposed for testing.
|
||||
*
|
||||
* @param {number} checkTargetValue- - The target value to check against.
|
||||
* @param {number} targetValue- - The target value to check against.
|
||||
* @param {RollOptions} rollOptions - Options that affect the checks outcome, e.g. different values for crits or whether slaying dice are used.
|
||||
* @param {RollProvider} provider - Service providing the various, real dice throws.
|
||||
* @param {Array<number>} dice - Optional array of dice values to consider instead of rolling new ones.
|
||||
*
|
||||
* @returns {RollResult} An object containing detailed information on the roll result.
|
||||
*/
|
||||
export function rollCheckMultipleDice(
|
||||
targetValue: number,
|
||||
rollOptions: Partial<RollOptions>,
|
||||
provider: RollProvider = new DS4RollProvider(),
|
||||
dice: Array<number> = null,
|
||||
): RollResult {
|
||||
const usedOptions = new DefaultRollOptions().mergeWith(rollOptions);
|
||||
const remainderTargetValue = targetValue % 20;
|
||||
const numberOfDice = Math.ceil(targetValue / 20);
|
||||
|
||||
const dice = provider.getNextRolls(numberOfDice);
|
||||
if (!dice || dice.length != numberOfDice) {
|
||||
dice = new DS4RollProvider().getNextRolls(numberOfDice);
|
||||
}
|
||||
const usedDice = dice;
|
||||
|
||||
const firstResult = dice[0];
|
||||
const firstResult = usedDice[0];
|
||||
const slayingDiceRepetition = isSlayingDiceRepetition(usedOptions);
|
||||
|
||||
// Slaying Dice require a different handling.
|
||||
if (firstResult >= usedOptions.minCritFail && !slayingDiceRepetition) {
|
||||
return new RollResult(0, RollResultStatus.CRITICAL_FAILURE, dice);
|
||||
return new RollResult(0, RollResultStatus.CRITICAL_FAILURE, usedDice, true);
|
||||
}
|
||||
|
||||
const [critSuccesses, otherRolls] = separateCriticalHits(dice, usedOptions);
|
||||
const [critSuccesses, otherRolls] = separateCriticalHits(usedDice, usedOptions);
|
||||
|
||||
const swapLastWithCrit: boolean = isDiceSwapNecessary([critSuccesses, otherRolls], remainderTargetValue);
|
||||
|
||||
|
@ -104,9 +110,9 @@ export function rollCheckMultipleDice(
|
|||
|
||||
const evaluationResult = calculateRollResult(sortedRollResults, remainderTargetValue, usedOptions);
|
||||
|
||||
if (usedOptions.useSlayingDice && firstResult <= usedOptions.maxCritSucc) {
|
||||
return new RollResult(evaluationResult, RollResultStatus.CRITICAL_SUCCESS, sortedRollResults);
|
||||
if (firstResult <= usedOptions.maxCritSuccess) {
|
||||
return new RollResult(evaluationResult, RollResultStatus.CRITICAL_SUCCESS, usedDice, true);
|
||||
} else {
|
||||
return new RollResult(evaluationResult, RollResultStatus.SUCCESS, sortedRollResults);
|
||||
return new RollResult(evaluationResult, RollResultStatus.SUCCESS, usedDice, true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
*/
|
||||
export class DS4RollProvider implements RollProvider {
|
||||
getNextRoll(): number {
|
||||
return new Roll("1d20").roll().total;
|
||||
const rand = CONFIG.Dice.randomUniform();
|
||||
return Math.ceil(rand * 20);
|
||||
}
|
||||
|
||||
getNextRolls(amount: number): Array<number> {
|
||||
|
|
|
@ -14,7 +14,7 @@ import { RollOptions } from "./roll-data";
|
|||
*/
|
||||
export function separateCriticalHits(dice: Array<number>, usedOptions: RollOptions): CritsAndNonCrits {
|
||||
const [critSuccesses, otherRolls] = partition(dice, (v: number) => {
|
||||
return v <= usedOptions.maxCritSucc;
|
||||
return v <= usedOptions.maxCritSuccess;
|
||||
}).map((a) => a.sort((r1, r2) => r2 - r1));
|
||||
|
||||
return [critSuccesses, otherRolls];
|
||||
|
@ -112,7 +112,7 @@ export function calculateRollResult(
|
|||
|
||||
return rollsAndMaxValues
|
||||
.map(([v, m]) => {
|
||||
return v <= rollOptions.maxCritSucc ? [m, m] : [v, m];
|
||||
return v <= rollOptions.maxCritSuccess ? [m, m] : [v, m];
|
||||
})
|
||||
.filter(([v, m]) => v <= m)
|
||||
.map(([v]) => v)
|
||||
|
|
Loading…
Reference in a new issue