import { DefaultRollOptions, RollOptions, RollResult, RollResultStatus } from "./roll-data"; import { DS4RollProvider, RollProvider } from "./roll-provider"; import { isDiceSwapNecessary, isSlayingDiceRepetition } from "./roll-utils"; export function ds4test(testValue: number, rollOptions: Partial<RollOptions> = {}): RollResult { const finalRollValue = testValue; if (finalRollValue <= 20) { return rollCheckSingleDie(finalRollValue, rollOptions); } else { return rollCheckMultipleDice(finalRollValue, rollOptions); } } export function rollCheckSingleDie( testValue: number, rollOptions: Partial<RollOptions>, provider: RollProvider = new DS4RollProvider(), ): RollResult { const usedOptions = new DefaultRollOptions().mergeWith(rollOptions); const roll = provider.getNextRoll(); const dice = [roll]; if (roll <= usedOptions.maxCritSucc) { return new RollResult(testValue, RollResultStatus.CRITICAL_SUCCESS, dice); } else if (roll >= usedOptions.minCritFail && !isSlayingDiceRepetition(usedOptions)) { return new RollResult(0, RollResultStatus.CRITICAL_FAILURE, dice); } else { if (roll <= testValue) { return new RollResult(roll, RollResultStatus.SUCCESS, dice); } else { return new RollResult(0, RollResultStatus.FAILURE, dice); } } } function separateCriticalHits(dice: Array<number>, usedOptions: RollOptions): [Array<number>, Array<number>] { const partitionCallback = (prev: [Array<number>, Array<number>], cur: number) => { if (cur <= usedOptions.maxCritSucc) { prev[0].push(cur); } else { prev[1].push(cur); } return prev; }; const [critSuccesses, otherRolls] = dice .reduce(partitionCallback, [[], []]) .map((a) => a.sort((r1, r2) => r2 - r1)); return [critSuccesses, otherRolls]; } export function rollCheckMultipleDice( testValue: number, rollOptions: Partial<RollOptions>, provider: RollProvider = new DS4RollProvider(), ): RollResult { const usedOptions = new DefaultRollOptions().mergeWith(rollOptions); const finalCheck = testValue % 20; const numberOfDice = Math.ceil(testValue / 20); const dice = provider.getNextRolls(numberOfDice); const firstResult = dice[0]; const slayingDiceRepetition = isSlayingDiceRepetition(usedOptions); // Slaying Dice require a different handling. if (firstResult >= usedOptions.minCritFail && !slayingDiceRepetition) { return new RollResult(0, RollResultStatus.CRITICAL_FAILURE, dice); } const [critSuccesses, otherRolls] = separateCriticalHits(dice, usedOptions); const swapLastWithCrit: boolean = isDiceSwapNecessary(critSuccesses, otherRolls, finalCheck); let sortedRollResults: Array<number>; if (swapLastWithCrit) { const diceToMove = critSuccesses[0]; const remainingSuccesses = critSuccesses.slice(1); sortedRollResults = remainingSuccesses.concat(otherRolls).concat([diceToMove]); } else { sortedRollResults = critSuccesses.concat(otherRolls); } const evaluationResult = sortedRollResults .map((value, index) => { if (index == numberOfDice - 1) { if (value <= usedOptions.maxCritSucc) { return finalCheck; } else if (value <= finalCheck) { return value; } else { return 0; } } else { if (value <= usedOptions.maxCritSucc) { return 20; } else { return value; } } }) .reduce((a, b) => a + b); if (usedOptions.useSlayingDice && firstResult <= usedOptions.maxCritSucc) { return new RollResult(evaluationResult, RollResultStatus.CRITICAL_SUCCESS, sortedRollResults); } else { return new RollResult(evaluationResult, RollResultStatus.SUCCESS, sortedRollResults); } }