2021-01-02 16:12:16 +01:00
import { RollOptions } from "./roll-data" ;
2021-01-04 21:21:06 +01:00
/ * *
* Separates critical hits ( "Coups" ) from throws , that get counted with their regular value .
*
* @internal
*
* @private_remarks
* This uses an internal implementation of a ` partition ` method . Don 't let typescript fool you, it will tell you that a partition method is available for Arrays, but that one' s imported globally from foundry ' s declarations and not available during the test stage !
*
* @param { Array < number > } dice - The dice values .
* @param { RollOptions } usedOptions - Options that affect the check ' s behaviour .
* @returns { [ Array < number > , Array < number > ] } A tuple containing two arrays of dice values , the first one containing all critical hits , the second one containing all others . Both arrays are sorted descendingby value .
* /
export function separateCriticalHits ( dice : Array < number > , usedOptions : RollOptions ) : CritsAndNonCrits {
const [ critSuccesses , otherRolls ] = partition ( dice , ( v : number ) = > {
return v <= usedOptions . maxCritSucc ;
} ) . map ( ( a ) = > a . sort ( ( r1 , r2 ) = > r2 - r1 ) ) ;
return [ critSuccesses , otherRolls ] ;
}
/ * *
* Helper type to properly bind combinations of critical and non critical dice .
* @internal
* /
type CritsAndNonCrits = [ Array < number > , Array < number > ] ;
/ * *
* Partition an array into two , following a predicate .
* @param { Array < T > } input The Array to split .
* @param { ( T ) = > boolean } predicate The predicate by which to split .
* @returns A tuple of two arrays , the first one containing all elements from ` input ` that matched the predicate , the second one containing those that don ' t .
* /
// TODO: Move to generic utils method?
function partition < T > ( input : Array < T > , predicate : ( v : T ) = > boolean ) {
return input . reduce (
( p : [ Array < T > , Array < T > ] , cur : T ) = > {
if ( predicate ( cur ) ) {
p [ 0 ] . push ( cur ) ;
} else {
p [ 1 ] . push ( cur ) ;
}
return p ;
} ,
[ [ ] , [ ] ] ,
) ;
}
/ * *
* Calculates if a critical success should be moved to the last position in order to maximize the check ' s result .
*
* @example
* With regular dice rolling rules and a check target number of 31 , the two dice 1 and 19 can get to a check result of 30 .
* This method would be called as follows :
* ` ` `
* isDiceSwapNecessary ( [ [ 1 ] , [ 19 ] ] , 11 )
* ` ` `
*
* @param { [ Array < number > , Array < number > ] } critsAndNonCrits the dice values thrown . It is assumed that both critical successes and other rolls are sorted descending .
* @param { number } remainingTargetValue the target value for the last dice , that is the only one that can be less than 20 .
* @returns { boolean } Bool indicating whether a critical success has to be used as the last dice .
* /
2021-01-02 16:12:16 +01:00
export function isDiceSwapNecessary (
2021-01-04 21:21:06 +01:00
[ critSuccesses , otherRolls ] : CritsAndNonCrits ,
2021-01-04 19:38:26 +01:00
remainingTargetValue : number ,
2021-01-02 16:12:16 +01:00
) : boolean {
if ( critSuccesses . length == 0 || otherRolls . length == 0 ) {
return false ;
}
const amountOfOtherRolls = otherRolls . length ;
const lastDice = otherRolls [ amountOfOtherRolls - 1 ] ;
2021-01-04 19:38:26 +01:00
if ( lastDice <= remainingTargetValue ) {
2021-01-02 16:12:16 +01:00
return false ;
}
2021-01-04 19:38:26 +01:00
return lastDice + remainingTargetValue > 20 ;
2021-01-02 16:12:16 +01:00
}
2021-01-04 21:21:06 +01:00
/ * *
* Checks if the options indicate that the current check is emerging from a crit success on a roll with slaying dice .
*
* @internal
*
* @param { RollOptions } opts the roll options to check against
* /
2021-01-02 16:12:16 +01:00
export function isSlayingDiceRepetition ( opts : RollOptions ) : boolean {
return opts . useSlayingDice && opts . slayingDiceRepetition ;
}
2021-01-04 19:38:26 +01:00
2021-01-04 21:21:06 +01:00
/ * *
* Calculate the check value of an array of dice , assuming the dice should be used in order of occurence .
*
* @internal
*
* @param assignedRollResults The dice values in the order of usage .
* @param remainderTargetValue Target value for the last dice ( the only one differing from ` 20 ` ) .
* @param rollOptions Config object containing options that change the way dice results are handled .
*
* @returns { number } The total check value .
* /
2021-01-04 19:38:26 +01:00
export function calculateRollResult (
assignedRollResults : Array < number > ,
remainderTargetValue : number ,
rollOptions : RollOptions ,
) : number {
const numberOfDice = assignedRollResults . length ;
const maxResultPerDie : Array < number > = Array ( numberOfDice ) . fill ( 20 ) ;
maxResultPerDie [ numberOfDice - 1 ] = remainderTargetValue ;
const rollsAndMaxValues = zip ( assignedRollResults , maxResultPerDie ) ;
return rollsAndMaxValues
. map ( ( [ v , m ] ) = > {
return v <= rollOptions . maxCritSucc ? [ m , m ] : [ v , m ] ;
} )
. filter ( ( [ v , m ] ) = > v <= m )
. map ( ( [ v ] ) = > v )
. reduce ( ( a , b ) = > a + b ) ;
}
// TODO: Move to generic utils method?
2021-01-04 21:21:06 +01:00
/ * *
* Zips two Arrays to an array of pairs of elements with corresponding indices . Excessive elements are dropped .
* @param { Array < T > } a1 First array to zip .
* @param { Array < U > } a2 Second array to zip .
*
* @typeParam T - Type of elements contained in ` a1 ` .
* @typeParam U - Type of elements contained in ` a2 ` .
*
* @returns { Array < [ T , U ] > } The array of pairs that had the same index in their source array .
* /
2021-01-04 19:38:26 +01:00
function zip < T , U > ( a1 : Array < T > , a2 : Array < U > ) : Array < [ T , U ] > {
if ( a1 . length <= a2 . length ) {
return a1 . map ( ( e1 , i ) = > [ e1 , a2 [ i ] ] ) ;
} else {
return a2 . map ( ( e2 , i ) = > [ a1 [ i ] , e2 ] ) ;
}
}