diff --git a/spec/support/ds4rolls/executor.spec.ts b/spec/support/ds4rolls/executor.spec.ts index 3417e6e6..705241a9 100644 --- a/spec/support/ds4rolls/executor.spec.ts +++ b/spec/support/ds4rolls/executor.spec.ts @@ -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): 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]), ); }); }); diff --git a/src/lang/en.json b/src/lang/en.json index 78675926..4f79f7ef 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -113,5 +113,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" } diff --git a/src/module/ds4.ts b/src/module/ds4.ts index 3e727a4c..a9d46ac2 100644 --- a/src/module/ds4.ts +++ b/src/module/ds4.ts @@ -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 }); diff --git a/src/module/rolls/check.ts b/src/module/rolls/check.ts new file mode 100644 index 00000000..5886813c --- /dev/null +++ b/src/module/rolls/check.ts @@ -0,0 +1,142 @@ +import { RollResult, RollResultStatus } from "./roll-data"; +import { ds4roll } from "./roll-executor"; + +interface TermData { + number: number; + faces: number; + modifiers: Array; + options: Record; +} + +/** + * 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) { + 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) + .map((r) => { + const intermediateResults = [r]; + + let checked = 0; + while (checked < intermediateResults.length) { + const r = (intermediateResults as Array)[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 + }; +} diff --git a/src/module/rolls/roll-data.ts b/src/module/rolls/roll-data.ts index e36fd6cd..964034c9 100644 --- a/src/module/rolls/roll-data.ts +++ b/src/module/rolls/roll-data.ts @@ -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) {} + constructor( + public result: number, + public status: RollResultStatus, + public dice: Array, + 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", } diff --git a/src/module/rolls/roll-executor.ts b/src/module/rolls/roll-executor.ts index 0a7f1694..72e6b2c4 100644 --- a/src/module/rolls/roll-executor.ts +++ b/src/module/rolls/roll-executor.ts @@ -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 optional, final option override that affect the checks outcome, e.g. different values for crits or whether slaying dice are used. + * @param {Array} dice optional, pass already thrown dice that are used instead of rolling new ones. */ -export function ds4roll(checkTargetValue: number, rollOptions: Partial = {}): RollResult { +export function ds4roll( + checkTargetValue: number, + rollOptions: Partial = {}, + dice: Array = 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} 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, - provider: RollProvider = new DS4RollProvider(), + dice: Array = 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} 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, - provider: RollProvider = new DS4RollProvider(), + dice: Array = 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); } } diff --git a/src/module/rolls/roll-provider.ts b/src/module/rolls/roll-provider.ts index 0781e5b0..86c55606 100644 --- a/src/module/rolls/roll-provider.ts +++ b/src/module/rolls/roll-provider.ts @@ -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 { diff --git a/src/module/rolls/roll-utils.ts b/src/module/rolls/roll-utils.ts index 09d618c3..a880a66d 100644 --- a/src/module/rolls/roll-utils.ts +++ b/src/module/rolls/roll-utils.ts @@ -14,7 +14,7 @@ import { RollOptions } from "./roll-data"; */ export function separateCriticalHits(dice: Array, 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)