Allow multiple dice and error check.
This commit is contained in:
parent
3dca6a5ae4
commit
22f53e5420
7 changed files with 161 additions and 175 deletions
|
@ -65,37 +65,37 @@ describe("DS4 Rolls with one die and slaying dice, followup throw.", () => {
|
||||||
|
|
||||||
describe("DS4 Rolls with one die and crit roll modifications.", () => {
|
describe("DS4 Rolls with one die and crit roll modifications.", () => {
|
||||||
it("Should do a crit success on `1`.", () => {
|
it("Should do a crit success on `1`.", () => {
|
||||||
expect(rollCheckSingleDie(4, { maxCritSucc: 2, minCritFail: 19 }, [1])).toEqual(
|
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFail: 19 }, [1])).toEqual(
|
||||||
new RollResult(4, RollResultStatus.CRITICAL_SUCCESS, [1]),
|
new RollResult(4, RollResultStatus.CRITICAL_SUCCESS, [1]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should do a crit success on `maxCritSucc`.", () => {
|
it("Should do a crit success on `maxCritSucc`.", () => {
|
||||||
expect(rollCheckSingleDie(4, { maxCritSucc: 2, minCritFail: 19 }, [2])).toEqual(
|
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFail: 19 }, [2])).toEqual(
|
||||||
new RollResult(4, RollResultStatus.CRITICAL_SUCCESS, [2]),
|
new RollResult(4, RollResultStatus.CRITICAL_SUCCESS, [2]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should do a success on lower edge case `3`.", () => {
|
it("Should do a success on lower edge case `3`.", () => {
|
||||||
expect(rollCheckSingleDie(4, { maxCritSucc: 2, minCritFail: 19 }, [3])).toEqual(
|
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFail: 19 }, [3])).toEqual(
|
||||||
new RollResult(3, RollResultStatus.SUCCESS, [3]),
|
new RollResult(3, RollResultStatus.SUCCESS, [3]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should do a success on upper edge case `18`.", () => {
|
it("Should do a success on upper edge case `18`.", () => {
|
||||||
expect(rollCheckSingleDie(4, { maxCritSucc: 2, minCritFail: 19 }, [18])).toEqual(
|
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFail: 19 }, [18])).toEqual(
|
||||||
new RollResult(0, RollResultStatus.FAILURE, [18]),
|
new RollResult(0, RollResultStatus.FAILURE, [18]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should do a crit fail on `minCritFail`.", () => {
|
it("Should do a crit fail on `minCritFail`.", () => {
|
||||||
expect(rollCheckSingleDie(4, { maxCritSucc: 2, minCritFail: 19 }, [19])).toEqual(
|
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFail: 19 }, [19])).toEqual(
|
||||||
new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [19]),
|
new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [19]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should do a crit fail on `20`", () => {
|
it("Should do a crit fail on `20`", () => {
|
||||||
expect(rollCheckSingleDie(4, { maxCritSucc: 2, minCritFail: 19 }, [20])).toEqual(
|
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFail: 19 }, [20])).toEqual(
|
||||||
new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [20]),
|
new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [20]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -171,37 +171,37 @@ describe("DS4 Rolls with multiple dice and no modifiers.", () => {
|
||||||
|
|
||||||
describe("DS4 Rolls with multiple dice and min/max modifiers.", () => {
|
describe("DS4 Rolls with multiple dice and min/max modifiers.", () => {
|
||||||
it("Should do a crit fail on `19` for first roll.", () => {
|
it("Should do a crit fail on `19` for first roll.", () => {
|
||||||
expect(rollCheckMultipleDice(48, { maxCritSucc: 2, minCritFail: 19 }, [19, 15, 6])).toEqual(
|
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFail: 19 }, [19, 15, 6])).toEqual(
|
||||||
new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [19, 15, 6]),
|
new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [19, 15, 6]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should succeed with all rolls crit successes (1 and 2).", () => {
|
it("Should succeed with all rolls crit successes (1 and 2).", () => {
|
||||||
expect(rollCheckMultipleDice(48, { maxCritSucc: 2, minCritFail: 19 }, [2, 1, 2])).toEqual(
|
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFail: 19 }, [2, 1, 2])).toEqual(
|
||||||
new RollResult(48, RollResultStatus.CRITICAL_SUCCESS, [2, 1, 2]),
|
new RollResult(48, RollResultStatus.CRITICAL_SUCCESS, [2, 1, 2]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should succeed with the last roll not being sufficient.", () => {
|
it("Should succeed with the last roll not being sufficient.", () => {
|
||||||
expect(rollCheckMultipleDice(48, { maxCritSucc: 2, minCritFail: 19 }, [15, 15, 15])).toEqual(
|
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFail: 19 }, [15, 15, 15])).toEqual(
|
||||||
new RollResult(30, RollResultStatus.SUCCESS, [15, 15, 15]),
|
new RollResult(30, RollResultStatus.SUCCESS, [15, 15, 15]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should succeed with the last roll a crit success `2`.", () => {
|
it("Should succeed with the last roll a crit success `2`.", () => {
|
||||||
expect(rollCheckMultipleDice(48, { maxCritSucc: 2, minCritFail: 19 }, [15, 15, 2])).toEqual(
|
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFail: 19 }, [15, 15, 2])).toEqual(
|
||||||
new RollResult(38, RollResultStatus.SUCCESS, [15, 15, 2]),
|
new RollResult(38, RollResultStatus.SUCCESS, [15, 15, 2]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should succeed with the last roll being `20` and one crit success '2'.", () => {
|
it("Should succeed with the last roll being `20` and one crit success '2'.", () => {
|
||||||
expect(rollCheckMultipleDice(48, { maxCritSucc: 2, minCritFail: 19 }, [15, 2, 20])).toEqual(
|
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFail: 19 }, [15, 2, 20])).toEqual(
|
||||||
new RollResult(43, RollResultStatus.SUCCESS, [15, 2, 20]),
|
new RollResult(43, RollResultStatus.SUCCESS, [15, 2, 20]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should succeed with the last roll being `19` and one crit success '2'.", () => {
|
it("Should succeed with the last roll being `19` and one crit success '2'.", () => {
|
||||||
expect(rollCheckMultipleDice(48, { maxCritSucc: 2, minCritFail: 19 }, [15, 2, 19])).toEqual(
|
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFail: 19 }, [15, 2, 19])).toEqual(
|
||||||
new RollResult(42, RollResultStatus.SUCCESS, [15, 2, 19]),
|
new RollResult(42, RollResultStatus.SUCCESS, [15, 2, 19]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -217,7 +217,7 @@ describe("DS4 Rolls with multiple dice and fail modifiers.", () => {
|
||||||
|
|
||||||
describe("DS4 Rolls with multiple dice and success modifiers.", () => {
|
describe("DS4 Rolls with multiple dice and success modifiers.", () => {
|
||||||
it("Should succeed with all rolls crit successes (1 and 2).", () => {
|
it("Should succeed with all rolls crit successes (1 and 2).", () => {
|
||||||
expect(rollCheckMultipleDice(48, { maxCritSucc: 2 }, [2, 1, 2])).toEqual(
|
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2 }, [2, 1, 2])).toEqual(
|
||||||
new RollResult(48, RollResultStatus.CRITICAL_SUCCESS, [2, 1, 2]),
|
new RollResult(48, RollResultStatus.CRITICAL_SUCCESS, [2, 1, 2]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -231,7 +231,7 @@ describe("DS4 Rolls with multiple and slaying dice, first throw", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should issue a critical success, even with resorting dice", () => {
|
it("Should issue a critical success, even with resorting dice", () => {
|
||||||
expect(rollCheckMultipleDice(48, { useSlayingDice: true, maxCritSucc: 2 }, [2, 19, 15])).toEqual(
|
expect(rollCheckMultipleDice(48, { useSlayingDice: true, maxCritSuccess: 2 }, [2, 19, 15])).toEqual(
|
||||||
new RollResult(42, RollResultStatus.CRITICAL_SUCCESS, [2, 19, 15]),
|
new RollResult(42, RollResultStatus.CRITICAL_SUCCESS, [2, 19, 15]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { DS4ActorSheet } from "./actor/actor-sheet";
|
||||||
import { DS4Item } from "./item/item";
|
import { DS4Item } from "./item/item";
|
||||||
import { DS4ItemSheet } from "./item/item-sheet";
|
import { DS4ItemSheet } from "./item/item-sheet";
|
||||||
import { DS4 } from "./config";
|
import { DS4 } from "./config";
|
||||||
import { DS4Roll } from "./rolls/ds4roll";
|
import { DS4Check } from "./rolls/check";
|
||||||
|
|
||||||
Hooks.once("init", async function () {
|
Hooks.once("init", async function () {
|
||||||
console.log(`DS4 | Initializing the DS4 Game System\n${DS4.ASCII}`);
|
console.log(`DS4 | Initializing the DS4 Game System\n${DS4.ASCII}`);
|
||||||
|
@ -23,11 +23,11 @@ Hooks.once("init", async function () {
|
||||||
CONFIG.Item.entityClass = DS4Item as typeof Item;
|
CONFIG.Item.entityClass = DS4Item as typeof Item;
|
||||||
|
|
||||||
// Configure Dice
|
// Configure Dice
|
||||||
CONFIG.Dice.types = [Die, DS4Roll];
|
CONFIG.Dice.types = [Die, DS4Check];
|
||||||
CONFIG.Dice.terms = {
|
CONFIG.Dice.terms = {
|
||||||
c: Coin,
|
c: Coin,
|
||||||
d: Die,
|
d: Die,
|
||||||
s: DS4Roll,
|
s: DS4Check,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Register sheet application classes
|
// Register sheet application classes
|
||||||
|
|
134
src/module/rolls/check.ts
Normal file
134
src/module/rolls/check.ts
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
import { RollResult, RollResultStatus } from "./roll-data";
|
||||||
|
import { ds4roll } from "./roll-executor";
|
||||||
|
|
||||||
|
interface TermData {
|
||||||
|
number: number;
|
||||||
|
faces: number;
|
||||||
|
modifiers: Array<string>;
|
||||||
|
options: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DS4Check extends DiceTerm {
|
||||||
|
constructor(termData: Partial<TermData>) {
|
||||||
|
super({
|
||||||
|
number: termData.number,
|
||||||
|
faces: termData.faces, // should be null
|
||||||
|
modifiers: termData.modifiers ?? [],
|
||||||
|
options: termData.options ?? {},
|
||||||
|
});
|
||||||
|
console.log("This @ constructor: ", termData);
|
||||||
|
|
||||||
|
// Store and parse target value.
|
||||||
|
const targetValueModifier = this.modifiers.filter((m) => m[0] === "v")[0];
|
||||||
|
const tvRgx = new RegExp("v([0-9]+)?");
|
||||||
|
const tvMatch = targetValueModifier?.match(tvRgx);
|
||||||
|
if (tvMatch) {
|
||||||
|
const [parseTargetValue] = tvMatch.slice(1);
|
||||||
|
this.targetValue = parseTargetValue ? parseInt(parseTargetValue) : DS4Check.DEFAULT_TARGET_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store and parse min/max crit
|
||||||
|
const critModifier = this.modifiers.filter((m) => m[0] === "c")[0];
|
||||||
|
const cmRgx = new RegExp("c([0-9]+)?,([0-9]+)?");
|
||||||
|
const cmMatch = critModifier?.match(cmRgx);
|
||||||
|
if (cmMatch) {
|
||||||
|
const [parseMaxCritSuccess, parseMinCritFailure] = cmMatch.slice(1);
|
||||||
|
this.maxCritSuccess = parseMaxCritSuccess
|
||||||
|
? parseInt(parseMaxCritSuccess)
|
||||||
|
: DS4Check.DEFAULT_MAX_CRIT_SUCCESS;
|
||||||
|
this.minCritFailure = parseMinCritFailure
|
||||||
|
? parseInt(parseMinCritFailure)
|
||||||
|
: DS4Check.DEFAULT_MIN_CRIT_FAILURE;
|
||||||
|
if (this.minCritFailure <= this.maxCritSuccess)
|
||||||
|
throw new SyntaxError("There's an overlap between Fumbles and Coups");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
success = null;
|
||||||
|
failure = null;
|
||||||
|
targetValue = DS4Check.DEFAULT_TARGET_VALUE;
|
||||||
|
minCritFailure = DS4Check.DEFAULT_MIN_CRIT_FAILURE;
|
||||||
|
maxCritSuccess = DS4Check.DEFAULT_MAX_CRIT_SUCCESS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
* @param param0
|
||||||
|
*
|
||||||
|
* @private_notes This gets only called once for the first roll.
|
||||||
|
*/
|
||||||
|
roll({ minimize = false, maximize = false } = {}): RollResult {
|
||||||
|
console.log(`This targetValue @ roll: ${this.targetValue}`);
|
||||||
|
const rollResult = this.rollWithDifferentBorders({ minimize, maximize });
|
||||||
|
this.results.push(rollResult);
|
||||||
|
if (rollResult.status == RollResultStatus.CRITICAL_SUCCESS) {
|
||||||
|
this.success = true;
|
||||||
|
} else if (rollResult.status == RollResultStatus.CRITICAL_FAILURE) {
|
||||||
|
this.failure = true;
|
||||||
|
}
|
||||||
|
return rollResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
rollWithDifferentBorders({ minimize = false, maximize = false } = {}, slayingDiceRepetition = false): RollResult {
|
||||||
|
const targetValueToUse = this.targetValue;
|
||||||
|
if (minimize) {
|
||||||
|
return new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [20], true);
|
||||||
|
} else if (maximize) {
|
||||||
|
const maximizedDice = Array(Math.ceil(targetValueToUse / 20)).fill(1);
|
||||||
|
return new RollResult(targetValueToUse, RollResultStatus.CRITICAL_SUCCESS, maximizedDice, true);
|
||||||
|
} else {
|
||||||
|
return ds4roll(targetValueToUse, {
|
||||||
|
maxCritSuccess: this.maxCritSuccess,
|
||||||
|
minCritFail: this.minCritFailure,
|
||||||
|
slayingDiceRepetition: slayingDiceRepetition,
|
||||||
|
useSlayingDice: slayingDiceRepetition,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Term Modifiers */
|
||||||
|
noop(): this {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DS4 only allows recursive explosions
|
||||||
|
explode(modifier: string): this {
|
||||||
|
const rgx = /[xX]/;
|
||||||
|
const match = modifier.match(rgx);
|
||||||
|
if (!match) return this;
|
||||||
|
|
||||||
|
this.results = (this.results as Array<RollResult>)
|
||||||
|
.map((r) => {
|
||||||
|
const intermedResult = [r];
|
||||||
|
|
||||||
|
let checked = 0;
|
||||||
|
while (checked < intermedResult.length) {
|
||||||
|
const r = (intermedResult as Array<RollResult>)[checked];
|
||||||
|
checked++;
|
||||||
|
if (!r.active) continue;
|
||||||
|
|
||||||
|
if (r.dice[0] <= this.maxCritSuccess) {
|
||||||
|
r.exploded = true;
|
||||||
|
const newRoll = this.rollWithDifferentBorders({}, true);
|
||||||
|
intermedResult.push(newRoll);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked > 1000) throw new Error("Maximum recursion depth for exploding dice roll exceeded");
|
||||||
|
}
|
||||||
|
return intermedResult;
|
||||||
|
})
|
||||||
|
.reduce((acc, cur) => {
|
||||||
|
return acc.concat(cur);
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
static readonly DEFAULT_TARGET_VALUE = 10;
|
||||||
|
static readonly DEFAULT_MAX_CRIT_SUCCESS = 1;
|
||||||
|
static readonly DEFAULT_MIN_CRIT_FAILURE = 20;
|
||||||
|
// TODO: add to Type declarations
|
||||||
|
static DENOMINATION = "s";
|
||||||
|
static MODIFIERS = {
|
||||||
|
x: "explode",
|
||||||
|
c: "noop", // Modifier is consumed in constructor for target value
|
||||||
|
v: "noop", // Modifier is consumed in constructor for target value
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,142 +0,0 @@
|
||||||
import { RollResult, RollResultStatus } from "./roll-data";
|
|
||||||
import { ds4roll } from "./roll-executor";
|
|
||||||
|
|
||||||
interface TermData {
|
|
||||||
number: number;
|
|
||||||
faces: number;
|
|
||||||
modifiers: Array<string>;
|
|
||||||
options: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DS4Roll extends DiceTerm {
|
|
||||||
constructor(termData: Partial<TermData>) {
|
|
||||||
super({
|
|
||||||
number: 1,
|
|
||||||
faces: termData.faces,
|
|
||||||
modifiers: termData.modifiers ?? [],
|
|
||||||
options: termData.options ?? {},
|
|
||||||
});
|
|
||||||
console.log("This @ constructor: ", termData);
|
|
||||||
|
|
||||||
const targetValueModifier = this.modifiers.filter((m) => m[0] === "v")[0] ?? "v" + DS4Roll.DEFAULT_TARGET_VALUE;
|
|
||||||
const rgx = new RegExp("v([0-9]+)?");
|
|
||||||
const match = targetValueModifier.match(rgx);
|
|
||||||
if (match) {
|
|
||||||
const [parseTargetValue] = match.slice(1);
|
|
||||||
this.targetValue = parseTargetValue ? parseInt(parseTargetValue) : DS4Roll.DEFAULT_TARGET_VALUE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
success = null;
|
|
||||||
failure = null;
|
|
||||||
number = 1;
|
|
||||||
targetValue = DS4Roll.DEFAULT_TARGET_VALUE;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @override
|
|
||||||
* @param param0
|
|
||||||
*
|
|
||||||
* @private_notes This gets only called once for the first roll.
|
|
||||||
*/
|
|
||||||
roll({ minimize = false, maximize = false } = {}): RollResult {
|
|
||||||
console.log(`This targetValue @ roll: ${this.targetValue}`);
|
|
||||||
const rollResult = this.rollWithDifferentBorders(1, 20, { 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(
|
|
||||||
maxCritSuccess: number,
|
|
||||||
minCritFail: number,
|
|
||||||
{ minimize = false, maximize = false } = {},
|
|
||||||
): RollResult {
|
|
||||||
const targetValueToUse = this.targetValue;
|
|
||||||
if (minimize) {
|
|
||||||
return new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [20], true);
|
|
||||||
} else if (maximize) {
|
|
||||||
const numberOfDice = Array(Math.ceil(targetValueToUse / 20)).fill(1);
|
|
||||||
return new RollResult(targetValueToUse, RollResultStatus.CRITICAL_SUCCESS, numberOfDice, true);
|
|
||||||
} else {
|
|
||||||
return ds4roll(targetValueToUse, { maxCritSucc: maxCritSuccess, minCritFail: minCritFail });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @override
|
|
||||||
*/
|
|
||||||
get values(): Array<number> {
|
|
||||||
return (this.results as Array<RollResult>)
|
|
||||||
.filter((r) => r.active)
|
|
||||||
.map((r) => r.dice)
|
|
||||||
.reduce((acc: Array<number>, cur: Array<number>) => {
|
|
||||||
return acc.concat(cur);
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Term Modifiers */
|
|
||||||
noop(): this {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
crits(modifier: string): this {
|
|
||||||
const rgx = new RegExp("c([0-9]+)?,([0-9]+)?");
|
|
||||||
const match = modifier.match(rgx);
|
|
||||||
if (!match) return this;
|
|
||||||
const [parseCritSucc, parsedCritFail] = match.slice(1);
|
|
||||||
|
|
||||||
const maxCritSuccess = parseCritSucc ? parseInt(parseCritSucc) : 1;
|
|
||||||
const minCritFail = parsedCritFail ? parseInt(parsedCritFail) : 20;
|
|
||||||
|
|
||||||
// noinspection UnnecessaryLocalVariableJS
|
|
||||||
const newResults: Array<RollResult> = (this.results as Array<RollResult>).map((r) => {
|
|
||||||
return ds4roll(this.targetValue, { minCritFail: minCritFail, maxCritSucc: maxCritSuccess }, r.dice);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.results = newResults;
|
|
||||||
}
|
|
||||||
|
|
||||||
// DS4 only allows recursive explosions
|
|
||||||
explode(modifier: string): this {
|
|
||||||
// There should only ever be a single dice in the results-array at this point!
|
|
||||||
if (this.results.length != 1) {
|
|
||||||
console.error(`Skipped explode for term ${this.expression}`);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rgx = /[xX]([0-9]+)?/;
|
|
||||||
const match = modifier.match(rgx);
|
|
||||||
if (!match) return this;
|
|
||||||
const [parsedCritSucc] = match.slice(1);
|
|
||||||
|
|
||||||
const maxCritSucc = parsedCritSucc ? parseInt(parsedCritSucc) : 1;
|
|
||||||
|
|
||||||
let checked = 0;
|
|
||||||
while (checked < this.results.length) {
|
|
||||||
const r = (this.results as Array<RollResult>)[checked];
|
|
||||||
checked++;
|
|
||||||
if (!r.active) continue;
|
|
||||||
|
|
||||||
if (r.dice[0] <= maxCritSucc) {
|
|
||||||
r.exploded = true;
|
|
||||||
const newRoll = this.rollWithDifferentBorders(maxCritSucc, 21);
|
|
||||||
this.results.push(newRoll);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checked > 1000) throw new Error("Maximum recursion depth for exploding dice roll exceeded");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static DEFAULT_TARGET_VALUE = 10;
|
|
||||||
// TODO: add to Type declarations
|
|
||||||
static DENOMINATION = "s";
|
|
||||||
static MODIFIERS = {
|
|
||||||
x: "explode",
|
|
||||||
c: "crits",
|
|
||||||
v: "noop", // Modifier is consumed in constructor for target value
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,12 +1,12 @@
|
||||||
export interface RollOptions {
|
export interface RollOptions {
|
||||||
maxCritSucc: number;
|
maxCritSuccess: number;
|
||||||
minCritFail: number;
|
minCritFail: number;
|
||||||
useSlayingDice: boolean;
|
useSlayingDice: boolean;
|
||||||
slayingDiceRepetition: boolean;
|
slayingDiceRepetition: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DefaultRollOptions implements RollOptions {
|
export class DefaultRollOptions implements RollOptions {
|
||||||
public maxCritSucc = 1;
|
public maxCritSuccess = 1;
|
||||||
public minCritFail = 20;
|
public minCritFail = 20;
|
||||||
public useSlayingDice = false;
|
public useSlayingDice = false;
|
||||||
public slayingDiceRepetition = false;
|
public slayingDiceRepetition = false;
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { calculateRollResult, isDiceSwapNecessary, isSlayingDiceRepetition, sepa
|
||||||
* Performs a roll against a check target number, e.g. for usage in battle, but not for herbs.
|
* 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 {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 {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(
|
export function ds4roll(
|
||||||
checkTargetValue: number,
|
checkTargetValue: number,
|
||||||
|
@ -26,12 +27,9 @@ export function ds4roll(
|
||||||
* This is not intended for direct usage. Use
|
* This is not intended for direct usage. Use
|
||||||
* {@link ds4roll | the function that is not bound to an amount of Dice} instead.
|
* {@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} 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 {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.
|
* @returns {RollResult} An object containing detailed information on the roll result.
|
||||||
*/
|
*/
|
||||||
|
@ -50,7 +48,7 @@ export function rollCheckSingleDie(
|
||||||
const usedDice = dice;
|
const usedDice = dice;
|
||||||
const rolledDie = usedDice[0];
|
const rolledDie = usedDice[0];
|
||||||
|
|
||||||
if (rolledDie <= usedOptions.maxCritSucc) {
|
if (rolledDie <= usedOptions.maxCritSuccess) {
|
||||||
return new RollResult(checkTargetValue, RollResultStatus.CRITICAL_SUCCESS, usedDice, true);
|
return new RollResult(checkTargetValue, RollResultStatus.CRITICAL_SUCCESS, usedDice, true);
|
||||||
} else if (rolledDie >= usedOptions.minCritFail && !isSlayingDiceRepetition(usedOptions)) {
|
} else if (rolledDie >= usedOptions.minCritFail && !isSlayingDiceRepetition(usedOptions)) {
|
||||||
return new RollResult(0, RollResultStatus.CRITICAL_FAILURE, usedDice, true);
|
return new RollResult(0, RollResultStatus.CRITICAL_FAILURE, usedDice, true);
|
||||||
|
@ -70,13 +68,9 @@ export function rollCheckSingleDie(
|
||||||
* This is not intended for direct usage. Use
|
* This is not intended for direct usage. Use
|
||||||
* {@link ds4roll | the function that is not bound to an amount of Dice} instead.
|
* {@link ds4roll | the function that is not bound to an amount of Dice} instead.
|
||||||
*
|
*
|
||||||
* @remarks
|
* @param {number} targetValue- - The target value to check against.
|
||||||
* 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 {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.
|
||||||
* @param {Array<number>} dice - Optional array of dice values to consider.
|
|
||||||
*
|
*
|
||||||
* @returns {RollResult} An object containing detailed information on the roll result.
|
* @returns {RollResult} An object containing detailed information on the roll result.
|
||||||
*/
|
*/
|
||||||
|
@ -118,7 +112,7 @@ export function rollCheckMultipleDice(
|
||||||
|
|
||||||
const evaluationResult = calculateRollResult(sortedRollResults, remainderTargetValue, usedOptions);
|
const evaluationResult = calculateRollResult(sortedRollResults, remainderTargetValue, usedOptions);
|
||||||
|
|
||||||
if (firstResult <= usedOptions.maxCritSucc) {
|
if (firstResult <= usedOptions.maxCritSuccess) {
|
||||||
return new RollResult(evaluationResult, RollResultStatus.CRITICAL_SUCCESS, usedDice, true);
|
return new RollResult(evaluationResult, RollResultStatus.CRITICAL_SUCCESS, usedDice, true);
|
||||||
} else {
|
} else {
|
||||||
return new RollResult(evaluationResult, RollResultStatus.SUCCESS, usedDice, true);
|
return new RollResult(evaluationResult, RollResultStatus.SUCCESS, usedDice, true);
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { RollOptions } from "./roll-data";
|
||||||
*/
|
*/
|
||||||
export function separateCriticalHits(dice: Array<number>, usedOptions: RollOptions): CritsAndNonCrits {
|
export function separateCriticalHits(dice: Array<number>, usedOptions: RollOptions): CritsAndNonCrits {
|
||||||
const [critSuccesses, otherRolls] = partition(dice, (v: number) => {
|
const [critSuccesses, otherRolls] = partition(dice, (v: number) => {
|
||||||
return v <= usedOptions.maxCritSucc;
|
return v <= usedOptions.maxCritSuccess;
|
||||||
}).map((a) => a.sort((r1, r2) => r2 - r1));
|
}).map((a) => a.sort((r1, r2) => r2 - r1));
|
||||||
|
|
||||||
return [critSuccesses, otherRolls];
|
return [critSuccesses, otherRolls];
|
||||||
|
@ -112,7 +112,7 @@ export function calculateRollResult(
|
||||||
|
|
||||||
return rollsAndMaxValues
|
return rollsAndMaxValues
|
||||||
.map(([v, m]) => {
|
.map(([v, m]) => {
|
||||||
return v <= rollOptions.maxCritSucc ? [m, m] : [v, m];
|
return v <= rollOptions.maxCritSuccess ? [m, m] : [v, m];
|
||||||
})
|
})
|
||||||
.filter(([v, m]) => v <= m)
|
.filter(([v, m]) => v <= m)
|
||||||
.map(([v]) => v)
|
.map(([v]) => v)
|
||||||
|
|
Loading…
Reference in a new issue