UNPKG

@randsum/dice

Version:

A flexible, type-safe dice roller for tabletop RPGs, game development, and probability simulations

283 lines (243 loc) 6.97 kB
import { CapModifier, DropModifier, ExplodeModifier, type ModifierOptions, type NumericRollBonus, ReplaceModifier, RerollModifier, UniqueModifier } from '@randsum/core' import { isNumericRollOptions } from '../guards/isNumericRollOptions' import type { CustomRollParams, DicePool, RollParams, RollResult } from '../types' import { calculateTotal } from './calculateTotal' import { coreRandom } from './coreRandom' import { coreSpreadRolls } from './coreSpreadRolls' export function rollResultFromDicePools(dicePools: DicePool): RollResult { const rawRolls = generateRawRolls(dicePools.dicePools) const modifiedRolls = generateModifiedRolls(dicePools, rawRolls) const modifiedValues = Object.values(modifiedRolls) return { ...dicePools, rawRolls, modifiedRolls, rawResult: Object.values(rawRolls).flat(), result: modifiedValues.map((pool) => pool.rolls).flat(), type: calculateDicePoolType(dicePools.dicePools), total: calculateTotal(modifiedValues.map((pool) => pool.total)) } as RollResult } function calculateDicePoolType( dicePools: DicePool['dicePools'] ): RollResult['type'] { const pools = Object.values(dicePools) if (pools.every((pool) => typeof pool.options.sides === 'number')) { return 'numerical' } if (pools.every((pool) => Array.isArray(pool.options.sides))) { return 'custom' } return 'mixed' } function isCustomParameters( poolParameters: RollParams ): poolParameters is CustomRollParams { return Array.isArray(poolParameters.options.sides) } /** * Apply a modifier to the current roll bonuses * * This optimized implementation avoids unnecessary object creation and function calls * by directly applying modifiers based on their type. * * @param key - The modifier key to apply * @param modifiers - All available modifiers * @param currentBonuses - The current roll bonuses to modify * @param rollParams - Parameters for the roll * @returns Modified roll bonuses */ function applyModifier( key: keyof ModifierOptions, modifiers: ModifierOptions, currentBonuses: NumericRollBonus, rollParams: { sides: number; quantity: number; rollOne: () => number } ): NumericRollBonus { const modifierValue = modifiers[key] if (modifierValue === undefined) { return currentBonuses } switch (key) { case 'plus': return { ...currentBonuses, simpleMathModifier: modifierValue as number } case 'minus': return { ...currentBonuses, simpleMathModifier: -(modifierValue as number) } case 'reroll': return new RerollModifier(modifiers.reroll).apply( currentBonuses, undefined, rollParams.rollOne ) case 'unique': return new UniqueModifier(modifiers.unique).apply( currentBonuses, { sides: rollParams.sides, quantity: rollParams.quantity }, rollParams.rollOne ) case 'replace': return new ReplaceModifier(modifiers.replace).apply(currentBonuses) case 'cap': return new CapModifier(modifiers.cap).apply(currentBonuses) case 'drop': return new DropModifier(modifiers.drop).apply(currentBonuses) case 'explode': return new ExplodeModifier(modifiers.explode).apply( currentBonuses, { sides: rollParams.sides, quantity: rollParams.quantity }, rollParams.rollOne ) default: throw new Error(`Unknown modifier: ${String(key)}`) } } /** * Generate modified roll results by applying modifiers to raw rolls * * This optimized implementation reduces unnecessary object creation and improves * performance by optimizing the modifier application order and using more efficient * data structures. * * @param dicePools - The dice pools to process * @param rawRolls - The raw roll results * @returns Modified roll results */ function generateModifiedRolls( dicePools: DicePool, rawRolls: RollResult['rawRolls'] ): RollResult['modifiedRolls'] { const result: RollResult['modifiedRolls'] = {} for (const [key, params] of Object.entries(dicePools.dicePools)) { const rolls = rawRolls[key] ?? [] if (isCustomParameters(params)) { result[key] = { total: calculateTotal(rolls), rolls } continue } const { sides, quantity = 1, modifiers = {} } = params.options if (Object.keys(modifiers).length === 0) { result[key] = { total: calculateTotal(rolls), rolls: rolls as number[] } continue } const rollOne = (): number => coreRandom(sides) const initialBonuses: NumericRollBonus = { simpleMathModifier: 0, rolls: rolls as number[] } let bonuses = initialBonuses if (modifiers.reroll) { bonuses = applyModifier('reroll', modifiers, bonuses, { sides, quantity, rollOne }) } if (modifiers.replace) { bonuses = applyModifier('replace', modifiers, bonuses, { sides, quantity, rollOne }) } if (modifiers.cap) { bonuses = applyModifier('cap', modifiers, bonuses, { sides, quantity, rollOne }) } if (modifiers.explode) { bonuses = applyModifier('explode', modifiers, bonuses, { sides, quantity, rollOne }) } if (modifiers.unique) { bonuses = applyModifier('unique', modifiers, bonuses, { sides, quantity, rollOne }) } if (modifiers.drop) { bonuses = applyModifier('drop', modifiers, bonuses, { sides, quantity, rollOne }) } if (modifiers.plus) { bonuses = applyModifier('plus', modifiers, bonuses, { sides, quantity, rollOne }) } if (modifiers.minus) { bonuses = applyModifier('minus', modifiers, bonuses, { sides, quantity, rollOne }) } result[key] = { rolls: bonuses.rolls, total: calculateTotal(bonuses.rolls, bonuses.simpleMathModifier) } } return result } /** * Generate raw roll results for all dice pools * * This optimized implementation reduces unnecessary object creation and improves * performance by using direct object property assignment instead of creating * intermediate arrays. * * @param dicePools - The dice pools to roll * @returns Raw roll results */ function generateRawRolls( dicePools: DicePool['dicePools'] ): RollResult['rawRolls'] { const result: RollResult['rawRolls'] = {} for (const [key, pool] of Object.entries(dicePools)) { const { options } = pool const quantity = options.quantity ?? 1 if (isNumericRollOptions(options)) { result[key] = coreSpreadRolls<number>(quantity, options.sides) } else { result[key] = coreSpreadRolls( quantity, options.sides.length, options.sides ) } } return result }