From c885d2d40586cf94a1ae2e9c3d0e106ca6fe0015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20R=C3=BCmpelein?= Date: Wed, 6 Jan 2021 19:15:56 +0100 Subject: [PATCH] Implement a custom DS4 "dice" class. Includes exploding and crit dice modifier, but not tested yet, as some required types are still missing. --- src/module/ds4.ts | 9 +++ src/module/rolls/ds4roll.ts | 95 +++++++++++++++++++++++++++++++ src/module/rolls/roll-data.ts | 1 + src/module/rolls/roll-executor.ts | 10 ++-- src/module/rolls/roll-provider.ts | 3 +- 5 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 src/module/rolls/ds4roll.ts diff --git a/src/module/ds4.ts b/src/module/ds4.ts index 358074b0..ddc135ae 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 { DS4Roll } from "./rolls/ds4roll"; 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, DS4Roll]; + CONFIG.Dice.terms = { + c: Coin, + d: Die, + s: DS4Roll, + }; + // Register sheet application classes Actors.unregisterSheet("core", ActorSheet); Actors.registerSheet("ds4", DS4ActorSheet, { makeDefault: true }); diff --git a/src/module/rolls/ds4roll.ts b/src/module/rolls/ds4roll.ts new file mode 100644 index 00000000..336fb354 --- /dev/null +++ b/src/module/rolls/ds4roll.ts @@ -0,0 +1,95 @@ +import { RollResult, RollResultStatus } from "./roll-data"; +import { ds4roll } from "./roll-executor"; + +export class DS4Roll extends DiceTerm { + constructor({ faces = 20, modifiers = [], options = {} } = {}) { + super({ number: 1, faces: faces, modifiers: modifiers, options: options }); + } + + /** + * @override + * @param param0 + */ + roll({ minimize = false, maximize = false } = {}): RollResult { + return this.rollWithDifferentBorders(1, 20, { minimize, maximize }); + } + + rollWithDifferentBorders( + maxCritSuccess: number, + minCritFail: number, + { minimize = false, maximize = false } = {}, + ): RollResult { + if (minimize) { + return new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [20], true); + } else if (maximize) { + return new RollResult( + this.faces, + RollResultStatus.CRITICAL_SUCCESS, + Array(Math.ceil(this.faces / 20)).fill(1), + true, + ); + } else { + return ds4roll(this.faces, { maxCritSucc: maxCritSuccess, minCritFail: minCritFail }); + } + } + + /** + * @override + */ + get values(): Array { + return (this.results as Array) + .filter((r) => r.active) + .map((r) => r.dice) + .reduce((acc: Array, cur: Array) => { + return acc.concat(cur); + }, []); + } + + /** Term Modifiers */ + crits(modifier: string): this { + const rgx: 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; + + const newResults: Array = (this.results as Array).map((r) => { + return ds4roll(this.faces, { 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) { + // TODO: Add 'expression' to types! + // 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)[checked]; + checked++; + if (!r.active) continue; + + if (r.dice[0] <= maxCritSucc) { + r.exploded = true; + this.rollWithDifferentBorders(maxCritSucc, 21); + } + + if (checked > 1000) throw new Error("Maximum recursion depth for explodign dice roll exceeded"); + } + } +} diff --git a/src/module/rolls/roll-data.ts b/src/module/rolls/roll-data.ts index f5b2b13e..516f8c61 100644 --- a/src/module/rolls/roll-data.ts +++ b/src/module/rolls/roll-data.ts @@ -22,6 +22,7 @@ export class RollResult { public status: RollResultStatus, public dice: Array, public active: boolean = true, + public exploded: boolean = false, ) {} } diff --git a/src/module/rolls/roll-executor.ts b/src/module/rolls/roll-executor.ts index 66d07690..b28ca4a3 100644 --- a/src/module/rolls/roll-executor.ts +++ b/src/module/rolls/roll-executor.ts @@ -48,15 +48,15 @@ export function rollCheckSingleDie( dice = [new DS4RollProvider().getNextRoll()]; } const usedDice = dice; - const roll = usedDice[0]; + const rolledDie = usedDice[0]; - if (roll <= usedOptions.maxCritSucc) { + if (rolledDie <= usedOptions.maxCritSucc) { return new RollResult(checkTargetValue, RollResultStatus.CRITICAL_SUCCESS, usedDice, true); - } else if (roll >= usedOptions.minCritFail && !isSlayingDiceRepetition(usedOptions)) { + } 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, usedDice, true); + if (rolledDie <= checkTargetValue) { + return new RollResult(rolledDie, RollResultStatus.SUCCESS, usedDice, true); } else { return new RollResult(0, RollResultStatus.FAILURE, 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 {