import { DefaultRollOptions, RollOptions, RollResult, RollResultStatus } from "./roll-data"; 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 = {}, dice: Array = null, ): RollResult { if (checkTargetValue <= 20) { return rollCheckSingleDie(checkTargetValue, rollOptions, dice); } else { return rollCheckMultipleDice(checkTargetValue, rollOptions, dice); } } /** * Performs a roll against a single die (CTN less than or equal 20). * * @internal * This is not intended for direct usage. Use * {@link ds4roll | the function that is not bound to an amount of Dice} instead. * * @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 {Array} 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, dice: Array = null, ): RollResult { const usedOptions = new DefaultRollOptions().mergeWith(rollOptions); 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.minCritFailure && !isSlayingDiceRepetition(usedOptions)) { return new RollResult(0, RollResultStatus.CRITICAL_FAILURE, usedDice, true); } else { if (rolledDie <= checkTargetValue) { return new RollResult(rolledDie, RollResultStatus.SUCCESS, usedDice, true); } else { return new RollResult(0, RollResultStatus.FAILURE, usedDice, true); } } } /** * Performs a roll against a multitude of die (CTN greater than 20). * * @internal * This is not intended for direct usage. Use * {@link ds4roll | the function that is not bound to an amount of Dice} instead. * * @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 {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, dice: Array = null, ): RollResult { const usedOptions = new DefaultRollOptions().mergeWith(rollOptions); const remainderTargetValue = targetValue % 20; const numberOfDice = Math.ceil(targetValue / 20); if (!dice || dice.length != numberOfDice) { dice = new DS4RollProvider().getNextRolls(numberOfDice); } const usedDice = dice; const firstResult = usedDice[0]; const slayingDiceRepetition = isSlayingDiceRepetition(usedOptions); // Slaying Dice require a different handling. if (firstResult >= usedOptions.minCritFailure && !slayingDiceRepetition) { return new RollResult(0, RollResultStatus.CRITICAL_FAILURE, usedDice, true); } const [critSuccesses, otherRolls] = separateCriticalHits(usedDice, usedOptions); const swapLastWithCrit: boolean = isDiceSwapNecessary([critSuccesses, otherRolls], remainderTargetValue); let sortedRollResults: Array; if (swapLastWithCrit) { const diceToMove = critSuccesses[0]; const remainingSuccesses = critSuccesses.slice(1); sortedRollResults = remainingSuccesses.concat(otherRolls).concat([diceToMove]); } else { sortedRollResults = critSuccesses.concat(otherRolls); } const evaluationResult = calculateRollResult(sortedRollResults, remainderTargetValue, usedOptions); if (firstResult <= usedOptions.maxCritSuccess) { return new RollResult(evaluationResult, RollResultStatus.CRITICAL_SUCCESS, usedDice, true); } else { return new RollResult(evaluationResult, RollResultStatus.SUCCESS, usedDice, true); } }