Files
5etools-mirror-2.github.io/js/encounterbuilder/encounterbuilder-helpers.js
TheGiddyLimit 8117ebddc5 v1.198.1
2024-01-01 19:34:49 +00:00

86 lines
3.7 KiB
JavaScript

export class EncounterBuilderHelpers {
static getCrCutoff (creatureMetas, partyMeta) {
creatureMetas = creatureMetas
.filter(creatureMeta => creatureMeta.getCrNumber() != null)
.sort((a, b) => SortUtil.ascSort(b.getCrNumber(), a.getCrNumber()));
if (!creatureMetas.length) return 0;
// no cutoff for CR 0-2
if (creatureMetas[0].getCrNumber() <= 2) return 0;
// ===============================================================================================================
// "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
// ===============================================================================================================
// "unless you think the weak monsters significantly contribute to the difficulty of the encounter"
// For player levels <5, always include every monster. We assume that levels 5> will have strong
// AoE/multiattack, allowing trash to be quickly cleared.
if (!partyMeta.isPartyLevelFivePlus()) return 0;
// Spread the CRs into a single array
const crValues = [];
creatureMetas.forEach(creatureMeta => {
const cr = creatureMeta.getCrNumber();
for (let i = 0; i < creatureMeta.count; ++i) crValues.push(cr);
});
// TODO(Future) allow this to be controlled by the user
let CR_THRESH_MODE = "statisticallySignificant";
switch (CR_THRESH_MODE) {
// "Statistically significant" method--note that this produces very passive filtering; the threshold is below
// the minimum CR in the vast majority of cases.
case "statisticallySignificant": {
const cpy = MiscUtil.copy(crValues)
.sort(SortUtil.ascSort);
const avg = cpy.mean();
const deviation = cpy.meanAbsoluteDeviation();
return avg - (deviation * 2);
}
case "5etools": {
// The ideal interpretation of this:
// "don't count any monsters whose challenge rating is significantly below the average
// challenge rating of the other monsters in the group"
// Is:
// Arrange the creatures in CR order, lowest to highest. Remove the lowest CR creature (or one of them, if there
// are ties). Calculate the average CR without this removed creature. If the removed creature's CR is
// "significantly below" this average, repeat the process with the next lowest CR creature.
// However, this can produce a stair-step pattern where our average CR keeps climbing as we remove more and more
// creatures. Therefore, only do this "remove creature -> calculate average CR" step _once_, and use the
// resulting average CR to calculate a cutoff.
const crMetas = [];
// If there's precisely one CR value, use it
if (crValues.length === 1) {
crMetas.push({
mean: crValues[0],
deviation: 0,
});
} else {
// Get an average CR for every possible encounter without one of the creatures in the encounter
for (let i = 0; i < crValues.length; ++i) {
const crValueFilt = crValues.filter((_, j) => i !== j);
const crMean = crValueFilt.mean();
const crStdDev = Math.sqrt((1 / crValueFilt.length) * crValueFilt.map(it => (it - crMean) ** 2).reduce((a, b) => a + b, 0));
crMetas.push({mean: crMean, deviation: crStdDev});
}
}
// Sort by descending average CR -> ascending deviation
crMetas.sort((a, b) => SortUtil.ascSort(b.mean, a.mean) || SortUtil.ascSort(a.deviation, b.deviation));
// "significantly below the average" -> cutoff at half the average
return crMetas[0].mean / 2;
}
default: return 0;
}
}
}