mirror of
https://github.com/Kornstalx/5etools-mirror-2.github.io.git
synced 2025-10-28 20:45:35 -05:00
86 lines
3.7 KiB
JavaScript
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;
|
|
}
|
|
}
|
|
}
|