js/bestiary.js
/**
 * @param data an array of {cr: n, count: m} objects
 * @param playerCount number of players in the party
 */
@param data an array of {cr: n, count: m} objects
@param playerCount number of players in the party
@param data an array of { cr : n count : m } objects @param playerCount number of players in the party
function calculateEncounterXp (data, playerCount = ECGEN_BASE_PLAYERS) {
	data = data.filter(it => it.cr !== 100).sort((a, b) => SortUtil.ascSort(b.cr, a.cr));
	let baseXp = 0;
	let relevantCount = 0;
	if (!data.length) return {baseXp: 0, relevantCount: 0, adjustedXp: 0};
	const crCutoff = (() => {
		let avg = null;
		let relevantCount = null;
		for (const it of data) {
			if (avg == null) {
				avg = it.cr;
				relevantCount = it.count;
			} else if (it.cr < avg / 2) {
				break;
			} else {
				avg = ((avg *= relevantCount) + (it.cr * it.count)) / (relevantCount + it.count);
				relevantCount += it.count;
			}
		}
		return avg;
	})();
	data.forEach(it => {
		if (it.cr >= crCutoff) relevantCount += it.count;
		baseXp += Parser.crToXpNumber(Parser.numberToCr(it.cr)) * it.count;
	});
	const playerAdjustedXpMult = Parser.numMonstersToXpMult(relevantCount, playerCount);
	const adjustedXp = playerAdjustedXpMult * baseXp;
	return {baseXp, relevantCount, adjustedXp, meta: {crCutoff, playerCount, playerAdjustedXpMult}};
}
function calculateEncounterXp ( data , playerCount = ECGEN_BASE_PLAYERS ) { data = data . filter ( it => it . cr !== 100 ) . sort ( ( a , b ) => SortUtil . ascSort ( b . cr , a . cr ) ) ; let baseXp = 0 ; let relevantCount = 0 ; if ( ! data . length ) return { baseXp : 0 , relevantCount : 0 , adjustedXp : 0 } ; const crCutoff = ( ( ) => { let avg = null ; let relevantCount = null ; for ( const it of data ) { if ( avg == null ) { avg = it . cr ; relevantCount = it . count ; } else if ( it . cr < avg / 2 ) { break ; } else { avg = ( ( avg *= relevantCount ) + ( it . cr * it . count ) ) / ( relevantCount + it . count ) ; relevantCount += it . count ; } } return avg ; } ) ( ) ; data . forEach ( it => { if ( it . cr >= crCutoff ) relevantCount += it . count ; baseXp += Parser . crToXpNumber ( Parser . numberToCr ( it . cr ) ) * it . count ; } ) ; const playerAdjustedXpMult = Parser . numMonstersToXpMult ( relevantCount , playerCount ) ; const adjustedXp = playerAdjustedXpMult * baseXp ; return { baseXp , relevantCount , adjustedXp , meta : { crCutoff , playerCount , playerAdjustedXpMult } } ; }
@param data an array of {cr: n, count: m} objects
 @param playerCount number of players in the party
@param data an array of { cr : n count : m } objects @param playerCount number of players in the party
// "When making this calculation, don't count any monsters whose challenge rating is significantly below the average // challenge rating of the other monsters in the group unless you think the weak monsters significantly contribute // to the difficulty of the encounter." -- DMG, p. 82 //   -> a shamelessly vague definition, so define it as: do a moving average of the CRs in largest-first order, //      stopping when the next CR is below half the current average; use the current average as the cutoff point.