Add 2014 DMG encounter difficulty calculation
All checks were successful
CI / check (push) Successful in 2m18s
CI / build-image (push) Successful in 17s

Support the 2014 DMG encounter difficulty as an alternative to the 5.5e
system behind the existing Rules Edition toggle. The 2014 system uses
Easy/Medium/Hard/Deadly thresholds, an encounter multiplier based on
monster count, and party size adjustment (×0.5–×5 range).

- Extract RulesEdition to its own domain module
- Refactor DifficultyTier to abstract numeric values (0–3)
- Restructure DifficultyResult with thresholds array
- Add 2014 XP thresholds table and encounter multiplier logic
- Wire edition from context into difficulty hooks
- Edition-aware labels in indicator and breakdown panel
- Show multiplier, adjusted XP, and party size note for 2014
- Rename settings label from "Conditions" to "Rules Edition"
- Update spec 008 with issue #23 requirements

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-04-04 14:52:23 +02:00
parent 94e1806112
commit 817cfddabc
17 changed files with 892 additions and 257 deletions

View File

@@ -46,185 +46,195 @@ function enemy(cr: string) {
return { cr, side: "enemy" as const };
}
describe("calculateEncounterDifficulty", () => {
it("returns trivial when monster XP is below Low threshold", () => {
describe("calculateEncounterDifficulty — 5.5e edition", () => {
it("returns tier 0 when monster XP is below Low threshold", () => {
// 4x level 1: Low = 200, Moderate = 300, High = 400
// 1x CR 0 = 0 XP -> trivial
const result = calculateEncounterDifficulty([
party(1),
party(1),
party(1),
party(1),
enemy("0"),
]);
expect(result.tier).toBe("trivial");
// 1x CR 0 = 0 XP -> tier 0
const result = calculateEncounterDifficulty(
[party(1), party(1), party(1), party(1), enemy("0")],
"5.5e",
);
expect(result.tier).toBe(0);
expect(result.totalMonsterXp).toBe(0);
expect(result.partyBudget).toEqual({
low: 200,
moderate: 300,
high: 400,
});
expect(result.thresholds).toEqual([
{ label: "Low", value: 200 },
{ label: "Moderate", value: 300 },
{ label: "High", value: 400 },
]);
expect(result.encounterMultiplier).toBeUndefined();
expect(result.adjustedXp).toBeUndefined();
expect(result.partySizeAdjusted).toBeUndefined();
});
it("returns low for 4x level 1 vs Bugbear (CR 1)", () => {
const result = calculateEncounterDifficulty([
party(1),
party(1),
party(1),
party(1),
enemy("1"),
]);
expect(result.tier).toBe("low");
it("returns tier 1 for 4x level 1 vs Bugbear (CR 1)", () => {
const result = calculateEncounterDifficulty(
[party(1), party(1), party(1), party(1), enemy("1")],
"5.5e",
);
expect(result.tier).toBe(1);
expect(result.totalMonsterXp).toBe(200);
});
it("returns moderate for 5x level 3 vs 1150 XP", () => {
it("returns tier 2 for 5x level 3 vs 1150 XP", () => {
// 5x level 3: Low = 750, Moderate = 1125, High = 2000
// CR 3 (700) + CR 2 (450) = 1150 XP >= 1125 Moderate
const result = calculateEncounterDifficulty([
party(3),
party(3),
party(3),
party(3),
party(3),
enemy("3"),
enemy("2"),
]);
expect(result.tier).toBe("moderate");
const result = calculateEncounterDifficulty(
[
party(3),
party(3),
party(3),
party(3),
party(3),
enemy("3"),
enemy("2"),
],
"5.5e",
);
expect(result.tier).toBe(2);
expect(result.totalMonsterXp).toBe(1150);
expect(result.partyBudget.moderate).toBe(1125);
expect(result.thresholds[1].value).toBe(1125);
});
it("returns high when XP meets High threshold", () => {
it("returns tier 3 when XP meets High threshold", () => {
// 4x level 1: High = 400
// 2x CR 1 = 400 XP -> High
const result = calculateEncounterDifficulty([
party(1),
party(1),
party(1),
party(1),
enemy("1"),
enemy("1"),
]);
expect(result.tier).toBe("high");
// 2x CR 1 = 400 XP -> tier 3
const result = calculateEncounterDifficulty(
[party(1), party(1), party(1), party(1), enemy("1"), enemy("1")],
"5.5e",
);
expect(result.tier).toBe(3);
expect(result.totalMonsterXp).toBe(400);
});
it("caps at high when XP far exceeds threshold", () => {
const result = calculateEncounterDifficulty([
party(1),
party(1),
party(1),
party(1),
enemy("30"),
]);
expect(result.tier).toBe("high");
it("caps at tier 3 when XP far exceeds threshold", () => {
const result = calculateEncounterDifficulty(
[party(1), party(1), party(1), party(1), enemy("30")],
"5.5e",
);
expect(result.tier).toBe(3);
expect(result.totalMonsterXp).toBe(155000);
});
it("handles mixed party levels", () => {
// 3x level 3 + 1x level 2
// Total: low=550, mod=825, high=1400
const result = calculateEncounterDifficulty([
party(3),
party(3),
party(3),
party(2),
enemy("3"),
const result = calculateEncounterDifficulty(
[party(3), party(3), party(3), party(2), enemy("3")],
"5.5e",
);
expect(result.thresholds).toEqual([
{ label: "Low", value: 550 },
{ label: "Moderate", value: 825 },
{ label: "High", value: 1400 },
]);
expect(result.partyBudget).toEqual({
low: 550,
moderate: 825,
high: 1400,
});
expect(result.totalMonsterXp).toBe(700);
expect(result.tier).toBe("low");
expect(result.tier).toBe(1);
});
it("returns trivial with no enemies", () => {
const result = calculateEncounterDifficulty([party(5), party(5)]);
expect(result.tier).toBe("trivial");
it("returns tier 0 with no enemies", () => {
const result = calculateEncounterDifficulty([party(5), party(5)], "5.5e");
expect(result.tier).toBe(0);
expect(result.totalMonsterXp).toBe(0);
});
it("returns high with no party levels (zero budget thresholds)", () => {
const result = calculateEncounterDifficulty([enemy("1")]);
expect(result.tier).toBe("high");
it("returns tier 3 with no party levels (zero budget thresholds)", () => {
const result = calculateEncounterDifficulty([enemy("1")], "5.5e");
expect(result.tier).toBe(3);
expect(result.totalMonsterXp).toBe(200);
expect(result.partyBudget).toEqual({ low: 0, moderate: 0, high: 0 });
expect(result.thresholds).toEqual([
{ label: "Low", value: 0 },
{ label: "Moderate", value: 0 },
{ label: "High", value: 0 },
]);
});
it("handles fractional CRs", () => {
const result = calculateEncounterDifficulty([
party(1),
party(1),
party(1),
party(1),
enemy("1/8"),
enemy("1/4"),
enemy("1/2"),
]);
const result = calculateEncounterDifficulty(
[
party(1),
party(1),
party(1),
party(1),
enemy("1/8"),
enemy("1/4"),
enemy("1/2"),
],
"5.5e",
);
expect(result.totalMonsterXp).toBe(175); // 25 + 50 + 100
expect(result.tier).toBe("trivial"); // 175 < 200 Low
expect(result.tier).toBe(0); // 175 < 200 Low
});
it("ignores unknown CRs (0 XP)", () => {
const result = calculateEncounterDifficulty([
party(1),
party(1),
party(1),
party(1),
enemy("unknown"),
]);
const result = calculateEncounterDifficulty(
[party(1), party(1), party(1), party(1), enemy("unknown")],
"5.5e",
);
expect(result.totalMonsterXp).toBe(0);
expect(result.tier).toBe("trivial");
expect(result.tier).toBe(0);
});
it("subtracts XP for party-side combatant with CR", () => {
// 4x level 1 party, 1 enemy CR 2 (450 XP), 1 party CR 1 (200 XP)
// Net = 450 - 200 = 250
const result = calculateEncounterDifficulty([
party(1),
party(1),
party(1),
party(1),
enemy("2"),
{ cr: "1", side: "party" },
]);
const result = calculateEncounterDifficulty(
[
party(1),
party(1),
party(1),
party(1),
enemy("2"),
{ cr: "1", side: "party" },
],
"5.5e",
);
expect(result.totalMonsterXp).toBe(250);
expect(result.tier).toBe("low"); // 250 >= 200 Low, < 300 Moderate
expect(result.tier).toBe(1); // 250 >= 200 Low, < 300 Moderate
});
it("floors net monster XP at 0", () => {
// Party ally has more XP than enemy
const result = calculateEncounterDifficulty([
party(1),
{ cr: "5", side: "party" }, // 1800 XP subtracted
enemy("1"), // 200 XP added
]);
const result = calculateEncounterDifficulty(
[
party(1),
{ cr: "5", side: "party" }, // 1800 XP subtracted
enemy("1"), // 200 XP added
],
"5.5e",
);
expect(result.totalMonsterXp).toBe(0);
expect(result.tier).toBe("trivial");
expect(result.tier).toBe(0);
});
it("dual contribution: combatant with both level and CR on party side", () => {
// Party combatant with level 1 AND CR 1 on party side
// Level contributes to budget, CR subtracts from monster XP
const result = calculateEncounterDifficulty([
{ level: 1, cr: "1", side: "party" }, // budget += lv1, monsterXp -= 200
enemy("2"), // monsterXp += 450
const result = calculateEncounterDifficulty(
[
{ level: 1, cr: "1", side: "party" }, // budget += lv1, monsterXp -= 200
enemy("2"), // monsterXp += 450
],
"5.5e",
);
expect(result.thresholds).toEqual([
{ label: "Low", value: 50 },
{ label: "Moderate", value: 75 },
{ label: "High", value: 100 },
]);
expect(result.partyBudget).toEqual({ low: 50, moderate: 75, high: 100 });
expect(result.totalMonsterXp).toBe(250); // 450 - 200
});
it("enemy-side combatant with level does NOT contribute to budget", () => {
const result = calculateEncounterDifficulty([
party(1),
{ level: 5, side: "enemy" }, // should not add to budget
enemy("1"),
]);
const result = calculateEncounterDifficulty(
[party(1), { level: 5, side: "enemy" }, enemy("1")],
"5.5e",
);
// Only level 1 party contributes to budget
expect(result.partyBudget).toEqual({ low: 50, moderate: 75, high: 100 });
expect(result.thresholds).toEqual([
{ label: "Low", value: 50 },
{ label: "Moderate", value: 75 },
{ label: "High", value: 100 },
]);
expect(result.totalMonsterXp).toBe(200);
});
@@ -232,19 +242,147 @@ describe("calculateEncounterDifficulty", () => {
// 2 party PCs (level 3), 1 party ally (CR 1, 200 XP), 2 enemies (CR 2, 450 each)
// Budget: 2x level 3 = low 300, mod 450, high 800
// Monster XP: 900 - 200 = 700
const result = calculateEncounterDifficulty([
party(3),
party(3),
{ cr: "1", side: "party" },
enemy("2"),
enemy("2"),
const result = calculateEncounterDifficulty(
[party(3), party(3), { cr: "1", side: "party" }, enemy("2"), enemy("2")],
"5.5e",
);
expect(result.thresholds).toEqual([
{ label: "Low", value: 300 },
{ label: "Moderate", value: 450 },
{ label: "High", value: 800 },
]);
expect(result.partyBudget).toEqual({
low: 300,
moderate: 450,
high: 800,
});
expect(result.totalMonsterXp).toBe(700);
expect(result.tier).toBe("moderate"); // 700 >= 450 Moderate, < 800 High
expect(result.tier).toBe(2); // 700 >= 450 Moderate, < 800 High
});
});
describe("calculateEncounterDifficulty — 2014 edition", () => {
it("uses 2014 XP thresholds table", () => {
// 4x level 1: Easy=100, Medium=200, Hard=300, Deadly=400
// 1 enemy CR 1 = 200 XP, x1 multiplier = 200 adjusted
// 200 >= 200 Medium → tier 1
const result = calculateEncounterDifficulty(
[party(1), party(1), party(1), party(1), enemy("1")],
"5e",
);
expect(result.tier).toBe(1);
expect(result.thresholds).toEqual([
{ label: "Easy", value: 100 },
{ label: "Medium", value: 200 },
{ label: "Hard", value: 300 },
{ label: "Deadly", value: 400 },
]);
});
it("applies encounter multiplier for 3 monsters (x2)", () => {
const result = calculateEncounterDifficulty(
[
party(1),
party(1),
party(1),
party(1),
enemy("1/8"),
enemy("1/8"),
enemy("1/8"),
],
"5e",
);
// Base: 75 XP, 3 monsters → x2 = 150 adjusted
expect(result.totalMonsterXp).toBe(75);
expect(result.encounterMultiplier).toBe(2);
expect(result.adjustedXp).toBe(150);
});
it("shifts multiplier up for fewer than 3 PCs", () => {
const result = calculateEncounterDifficulty(
[party(1), party(1), enemy("1")],
"5e",
);
// 1 monster, 2 PCs → base x1 shifts up to x1.5
expect(result.encounterMultiplier).toBe(1.5);
expect(result.partySizeAdjusted).toBe(true);
});
it("shifts multiplier down for 6+ PCs", () => {
const result = calculateEncounterDifficulty(
[
party(1),
party(1),
party(1),
party(1),
party(1),
party(1),
enemy("1"),
enemy("1"),
enemy("1"),
],
"5e",
);
// 3 monsters, 6 PCs → base x2 shifts down to x1.5
expect(result.encounterMultiplier).toBe(1.5);
expect(result.partySizeAdjusted).toBe(true);
});
it("shifts multiplier to x5 for 15+ monsters with <3 PCs", () => {
const result = calculateEncounterDifficulty(
[party(1), party(1), ...Array.from({ length: 15 }, () => enemy("0"))],
"5e",
);
// 15+ monsters = x4 base, shift up → x5
expect(result.encounterMultiplier).toBe(5);
expect(result.partySizeAdjusted).toBe(true);
});
it("shifts multiplier to x0.5 for 1 monster with 6+ PCs", () => {
const result = calculateEncounterDifficulty(
[party(1), party(1), party(1), party(1), party(1), party(1), enemy("1")],
"5e",
);
expect(result.encounterMultiplier).toBe(0.5);
expect(result.partySizeAdjusted).toBe(true);
});
it("only counts enemy-side combatants for monster count", () => {
const result = calculateEncounterDifficulty(
[
party(1),
party(1),
party(1),
party(1),
{ cr: "1", side: "party" },
enemy("1"),
enemy("1"),
enemy("1"),
],
"5e",
);
// 3 enemy monsters → x2, NOT 4
expect(result.encounterMultiplier).toBe(2);
});
it("returns tier 0 when adjusted XP meets Easy but not Medium", () => {
// 4x level 1: Easy=100, Medium=200
// 1 enemy CR 1/2 = 100 XP, x1 multiplier = 100 adjusted
// 100 >= Easy(100) but < Medium(200) → tier 0
const result = calculateEncounterDifficulty(
[party(1), party(1), party(1), party(1), enemy("1/2")],
"5e",
);
expect(result.tier).toBe(0);
expect(result.adjustedXp).toBe(100);
});
it("returns no party size adjustment for standard party (3-5)", () => {
const result = calculateEncounterDifficulty(
[party(1), party(1), party(1), party(1), enemy("1")],
"5e",
);
expect(result.partySizeAdjusted).toBe(false);
});
it("returns undefined multiplier/adjustedXp for 5.5e", () => {
const result = calculateEncounterDifficulty([party(1), enemy("1")], "5.5e");
expect(result.encounterMultiplier).toBeUndefined();
expect(result.adjustedXp).toBeUndefined();
});
});

View File

@@ -17,7 +17,7 @@ export type ConditionId =
| "stunned"
| "unconscious";
export type RulesEdition = "5e" | "5.5e";
import type { RulesEdition } from "./rules-edition.js";
export interface ConditionDefinition {
readonly id: ConditionId;

View File

@@ -1,13 +1,23 @@
export type DifficultyTier = "trivial" | "low" | "moderate" | "high";
import type { RulesEdition } from "./rules-edition.js";
/** Abstract difficulty severity: 0 = negligible, 3 = maximum. Maps to filled bar count. */
export type DifficultyTier = 0 | 1 | 2 | 3;
export interface DifficultyThreshold {
readonly label: string;
readonly value: number;
}
export interface DifficultyResult {
readonly tier: DifficultyTier;
readonly totalMonsterXp: number;
readonly partyBudget: {
readonly low: number;
readonly moderate: number;
readonly high: number;
};
readonly thresholds: readonly DifficultyThreshold[];
/** 2014 only: the encounter multiplier applied to base monster XP. */
readonly encounterMultiplier: number | undefined;
/** 2014 only: monster XP after applying the encounter multiplier. */
readonly adjustedXp: number | undefined;
/** 2014 only: true when the multiplier was shifted due to party size (<3 or 6+). */
readonly partySizeAdjusted: boolean | undefined;
}
/** Maps challenge rating strings to XP values (standard 5e). */
@@ -74,6 +84,82 @@ const XP_BUDGET_PER_CHARACTER: Readonly<
20: { low: 6400, moderate: 13200, high: 22000 },
};
/** Maps character level (1-20) to XP thresholds (2014 DMG). */
const XP_THRESHOLDS_2014: Readonly<
Record<number, { easy: number; medium: number; hard: number; deadly: number }>
> = {
1: { easy: 25, medium: 50, hard: 75, deadly: 100 },
2: { easy: 50, medium: 100, hard: 150, deadly: 200 },
3: { easy: 75, medium: 150, hard: 225, deadly: 400 },
4: { easy: 125, medium: 250, hard: 375, deadly: 500 },
5: { easy: 250, medium: 500, hard: 750, deadly: 1100 },
6: { easy: 300, medium: 600, hard: 900, deadly: 1400 },
7: { easy: 350, medium: 750, hard: 1100, deadly: 1700 },
8: { easy: 450, medium: 900, hard: 1400, deadly: 2100 },
9: { easy: 550, medium: 1100, hard: 1600, deadly: 2400 },
10: { easy: 600, medium: 1200, hard: 1900, deadly: 2800 },
11: { easy: 800, medium: 1600, hard: 2400, deadly: 3600 },
12: { easy: 1000, medium: 2000, hard: 3000, deadly: 4500 },
13: { easy: 1100, medium: 2200, hard: 3400, deadly: 5100 },
14: { easy: 1250, medium: 2500, hard: 3800, deadly: 5700 },
15: { easy: 1400, medium: 2800, hard: 4300, deadly: 6400 },
16: { easy: 1600, medium: 3200, hard: 4800, deadly: 7200 },
17: { easy: 2000, medium: 3900, hard: 5900, deadly: 8800 },
18: { easy: 2100, medium: 4200, hard: 6300, deadly: 9500 },
19: { easy: 2400, medium: 4900, hard: 7300, deadly: 10900 },
20: { easy: 2800, medium: 5700, hard: 8500, deadly: 12700 },
};
/** 2014 encounter multiplier by number of enemy-side monsters. */
const ENCOUNTER_MULTIPLIER_TABLE: readonly {
max: number;
multiplier: number;
}[] = [
{ max: 1, multiplier: 1 },
{ max: 2, multiplier: 1.5 },
{ max: 6, multiplier: 2 },
{ max: 10, multiplier: 2.5 },
{ max: 14, multiplier: 3 },
{ max: Number.POSITIVE_INFINITY, multiplier: 4 },
];
/**
* Multiplier values in ascending order for party size shifting.
* Extends beyond the base table: x0.5 (6+ PCs, 1 monster) and x5 (<3 PCs, 15+ monsters)
* per 2014 DMG party size adjustment rules.
*/
const MULTIPLIER_STEPS = [0.5, 1, 1.5, 2, 2.5, 3, 4, 5] as const;
/** Index into MULTIPLIER_STEPS for each base table entry (before party size adjustment). */
const BASE_STEP_INDEX = [1, 2, 3, 4, 5, 6] as const;
function getEncounterMultiplier(
monsterCount: number,
partySize: number,
): { multiplier: number; partySizeAdjusted: boolean } {
const tableIndex = ENCOUNTER_MULTIPLIER_TABLE.findIndex(
(entry) => monsterCount <= entry.max,
);
let stepIndex: number =
BASE_STEP_INDEX[
tableIndex === -1 ? BASE_STEP_INDEX.length - 1 : tableIndex
];
let partySizeAdjusted = false;
if (partySize < 3) {
stepIndex = Math.min(stepIndex + 1, MULTIPLIER_STEPS.length - 1);
partySizeAdjusted = true;
} else if (partySize >= 6) {
stepIndex = Math.max(stepIndex - 1, 0);
partySizeAdjusted = true;
}
return {
multiplier: MULTIPLIER_STEPS[stepIndex] as number,
partySizeAdjusted,
};
}
/** All standard 5e challenge rating strings, in ascending order. */
export const VALID_CR_VALUES: readonly string[] = Object.keys(CR_TO_XP);
@@ -90,14 +176,66 @@ export interface CombatantDescriptor {
function determineTier(
xp: number,
low: number,
moderate: number,
high: number,
tierThresholds: readonly number[],
): DifficultyTier {
if (xp >= high) return "high";
if (xp >= moderate) return "moderate";
if (xp >= low) return "low";
return "trivial";
for (let i = tierThresholds.length - 1; i >= 0; i--) {
if (xp >= tierThresholds[i]) return (i + 1) as DifficultyTier;
}
return 0;
}
function accumulateBudget5_5e(levels: readonly number[]) {
const budget = { low: 0, moderate: 0, high: 0 };
for (const level of levels) {
const b = XP_BUDGET_PER_CHARACTER[level];
if (b) {
budget.low += b.low;
budget.moderate += b.moderate;
budget.high += b.high;
}
}
return budget;
}
function accumulateBudget2014(levels: readonly number[]) {
const budget = { easy: 0, medium: 0, hard: 0, deadly: 0 };
for (const level of levels) {
const b = XP_THRESHOLDS_2014[level];
if (b) {
budget.easy += b.easy;
budget.medium += b.medium;
budget.hard += b.hard;
budget.deadly += b.deadly;
}
}
return budget;
}
function scanCombatants(combatants: readonly CombatantDescriptor[]) {
let totalMonsterXp = 0;
let monsterCount = 0;
const partyLevels: number[] = [];
for (const c of combatants) {
if (c.level !== undefined && c.side === "party") {
partyLevels.push(c.level);
}
if (c.cr !== undefined) {
const xp = crToXp(c.cr);
if (c.side === "enemy") {
totalMonsterXp += xp;
monsterCount++;
} else {
totalMonsterXp -= xp;
}
}
}
return {
totalMonsterXp: Math.max(0, totalMonsterXp),
monsterCount,
partyLevels,
};
}
/**
@@ -107,41 +245,54 @@ function determineTier(
*/
export function calculateEncounterDifficulty(
combatants: readonly CombatantDescriptor[],
edition: RulesEdition,
): DifficultyResult {
let budgetLow = 0;
let budgetModerate = 0;
let budgetHigh = 0;
let totalMonsterXp = 0;
const { totalMonsterXp, monsterCount, partyLevels } =
scanCombatants(combatants);
for (const c of combatants) {
if (c.level !== undefined && c.side === "party") {
const budget = XP_BUDGET_PER_CHARACTER[c.level];
if (budget) {
budgetLow += budget.low;
budgetModerate += budget.moderate;
budgetHigh += budget.high;
}
}
if (c.cr !== undefined) {
const xp = crToXp(c.cr);
if (c.side === "enemy") {
totalMonsterXp += xp;
} else {
totalMonsterXp -= xp;
}
}
if (edition === "5.5e") {
const budget = accumulateBudget5_5e(partyLevels);
const thresholds: DifficultyThreshold[] = [
{ label: "Low", value: budget.low },
{ label: "Moderate", value: budget.moderate },
{ label: "High", value: budget.high },
];
return {
tier: determineTier(totalMonsterXp, [
budget.low,
budget.moderate,
budget.high,
]),
totalMonsterXp,
thresholds,
encounterMultiplier: undefined,
adjustedXp: undefined,
partySizeAdjusted: undefined,
};
}
totalMonsterXp = Math.max(0, totalMonsterXp);
// 2014 edition
const budget = accumulateBudget2014(partyLevels);
const { multiplier: encounterMultiplier, partySizeAdjusted } =
getEncounterMultiplier(monsterCount, partyLevels.length);
const adjustedXp = Math.round(totalMonsterXp * encounterMultiplier);
const thresholds: DifficultyThreshold[] = [
{ label: "Easy", value: budget.easy },
{ label: "Medium", value: budget.medium },
{ label: "Hard", value: budget.hard },
{ label: "Deadly", value: budget.deadly },
];
return {
tier: determineTier(totalMonsterXp, budgetLow, budgetModerate, budgetHigh),
tier: determineTier(adjustedXp, [
budget.medium,
budget.hard,
budget.deadly,
]),
totalMonsterXp,
partyBudget: {
low: budgetLow,
moderate: budgetModerate,
high: budgetHigh,
},
thresholds,
encounterMultiplier,
adjustedXp,
partySizeAdjusted,
};
}

View File

@@ -16,7 +16,6 @@ export {
type ConditionId,
getConditionDescription,
getConditionsForEdition,
type RulesEdition,
VALID_CONDITION_IDS,
} from "./conditions.js";
export {
@@ -53,6 +52,7 @@ export {
calculateEncounterDifficulty,
crToXp,
type DifficultyResult,
type DifficultyThreshold,
type DifficultyTier,
VALID_CR_VALUES,
} from "./encounter-difficulty.js";
@@ -110,6 +110,7 @@ export {
rollInitiative,
selectRoll,
} from "./roll-initiative.js";
export type { RulesEdition } from "./rules-edition.js";
export { type SetAcSuccess, setAc } from "./set-ac.js";
export { type SetCrSuccess, setCr } from "./set-cr.js";
export { type SetHpSuccess, setHp } from "./set-hp.js";

View File

@@ -0,0 +1 @@
export type RulesEdition = "5e" | "5.5e";