Add combatant side assignment for encounter difficulty
Combatants can now be assigned to party or enemy side via a toggle in the difficulty breakdown panel. Party-side NPCs subtract their XP from the encounter total, letting allied NPCs reduce difficulty. PCs default to party, non-PCs to enemy — users who don't use sides see no change. Side persists across reload and export/import. Closes #22 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -82,43 +82,61 @@ export function crToXp(cr: string): number {
|
||||
return CR_TO_XP[cr] ?? 0;
|
||||
}
|
||||
|
||||
export interface CombatantDescriptor {
|
||||
readonly level?: number;
|
||||
readonly cr?: string;
|
||||
readonly side: "party" | "enemy";
|
||||
}
|
||||
|
||||
function determineTier(
|
||||
xp: number,
|
||||
low: number,
|
||||
moderate: number,
|
||||
high: number,
|
||||
): DifficultyTier {
|
||||
if (xp >= high) return "high";
|
||||
if (xp >= moderate) return "moderate";
|
||||
if (xp >= low) return "low";
|
||||
return "trivial";
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates encounter difficulty from party levels and monster CRs.
|
||||
* Both arrays should be pre-filtered (only PCs with levels, only bestiary-linked monsters).
|
||||
* Calculates encounter difficulty from combatant descriptors.
|
||||
* Party-side combatants with level contribute to the budget.
|
||||
* Enemy-side combatants with CR add XP; party-side with CR subtract XP (floored at 0).
|
||||
*/
|
||||
export function calculateEncounterDifficulty(
|
||||
partyLevels: readonly number[],
|
||||
monsterCrs: readonly string[],
|
||||
combatants: readonly CombatantDescriptor[],
|
||||
): DifficultyResult {
|
||||
let budgetLow = 0;
|
||||
let budgetModerate = 0;
|
||||
let budgetHigh = 0;
|
||||
let totalMonsterXp = 0;
|
||||
|
||||
for (const level of partyLevels) {
|
||||
const budget = XP_BUDGET_PER_CHARACTER[level];
|
||||
if (budget) {
|
||||
budgetLow += budget.low;
|
||||
budgetModerate += budget.moderate;
|
||||
budgetHigh += budget.high;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let totalMonsterXp = 0;
|
||||
for (const cr of monsterCrs) {
|
||||
totalMonsterXp += crToXp(cr);
|
||||
}
|
||||
|
||||
let tier: DifficultyTier = "trivial";
|
||||
if (totalMonsterXp >= budgetHigh) {
|
||||
tier = "high";
|
||||
} else if (totalMonsterXp >= budgetModerate) {
|
||||
tier = "moderate";
|
||||
} else if (totalMonsterXp >= budgetLow) {
|
||||
tier = "low";
|
||||
}
|
||||
totalMonsterXp = Math.max(0, totalMonsterXp);
|
||||
|
||||
return {
|
||||
tier,
|
||||
tier: determineTier(totalMonsterXp, budgetLow, budgetModerate, budgetHigh),
|
||||
totalMonsterXp,
|
||||
partyBudget: {
|
||||
low: budgetLow,
|
||||
|
||||
Reference in New Issue
Block a user