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:
@@ -106,7 +106,7 @@ describe("useDifficultyBreakdown", () => {
|
||||
expect(result.current).toBeNull();
|
||||
});
|
||||
|
||||
it("returns per-combatant entries with correct data", async () => {
|
||||
it("returns per-combatant entries split by side", async () => {
|
||||
const wrapper = makeWrapper({
|
||||
encounter: buildEncounter({
|
||||
combatants: [
|
||||
@@ -145,29 +145,34 @@ describe("useDifficultyBreakdown", () => {
|
||||
const breakdown = result.current;
|
||||
expect(breakdown).not.toBeNull();
|
||||
expect(breakdown?.pcCount).toBe(1);
|
||||
// CR 1/4 = 50 + CR 2 = 450 → total 500
|
||||
// CR 1/4 = 50 + CR 2 = 450 -> total 500
|
||||
expect(breakdown?.totalMonsterXp).toBe(500);
|
||||
expect(breakdown?.combatants).toHaveLength(3);
|
||||
|
||||
// Bestiary combatant
|
||||
const goblin = breakdown?.combatants[0];
|
||||
// PC in party column
|
||||
expect(breakdown?.partyCombatants).toHaveLength(1);
|
||||
expect(breakdown?.partyCombatants[0].combatant.name).toBe("Hero");
|
||||
expect(breakdown?.partyCombatants[0].side).toBe("party");
|
||||
expect(breakdown?.partyCombatants[0].level).toBe(5);
|
||||
|
||||
// Enemies: goblin, thug, bandit
|
||||
expect(breakdown?.enemyCombatants).toHaveLength(3);
|
||||
|
||||
const goblin = breakdown?.enemyCombatants[0];
|
||||
expect(goblin?.cr).toBe("1/4");
|
||||
expect(goblin?.xp).toBe(50);
|
||||
expect(goblin?.source).toBe("SRD");
|
||||
expect(goblin?.editable).toBe(false);
|
||||
expect(goblin?.side).toBe("enemy");
|
||||
|
||||
// Custom with CR
|
||||
const thug = breakdown?.combatants[1];
|
||||
const thug = breakdown?.enemyCombatants[1];
|
||||
expect(thug?.cr).toBe("2");
|
||||
expect(thug?.xp).toBe(450);
|
||||
expect(thug?.source).toBeNull();
|
||||
expect(thug?.editable).toBe(true);
|
||||
|
||||
// Custom without CR
|
||||
const bandit = breakdown?.combatants[2];
|
||||
const bandit = breakdown?.enemyCombatants[2];
|
||||
expect(bandit?.cr).toBeNull();
|
||||
expect(bandit?.xp).toBeNull();
|
||||
expect(bandit?.source).toBeNull();
|
||||
expect(bandit?.editable).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -203,16 +208,15 @@ describe("useDifficultyBreakdown", () => {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// With no bestiary creatures loaded, the Ghost has null CR
|
||||
const breakdown = result.current;
|
||||
expect(breakdown).not.toBeNull();
|
||||
const ghost = breakdown?.combatants[0];
|
||||
const ghost = breakdown?.enemyCombatants[0];
|
||||
expect(ghost?.cr).toBeNull();
|
||||
expect(ghost?.xp).toBeNull();
|
||||
expect(ghost?.editable).toBe(false);
|
||||
});
|
||||
|
||||
it("excludes PC combatants from breakdown entries", async () => {
|
||||
it("PC combatants appear in partyCombatants with level", async () => {
|
||||
const wrapper = makeWrapper({
|
||||
encounter: buildEncounter({
|
||||
combatants: [
|
||||
@@ -239,8 +243,53 @@ describe("useDifficultyBreakdown", () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current?.combatants).toHaveLength(1);
|
||||
expect(result.current?.combatants[0].combatant.name).toBe("Goblin");
|
||||
expect(result.current?.partyCombatants).toHaveLength(1);
|
||||
expect(result.current?.partyCombatants[0].combatant.name).toBe("Hero");
|
||||
expect(result.current?.partyCombatants[0].level).toBe(1);
|
||||
expect(result.current?.partyCombatants[0].side).toBe("party");
|
||||
});
|
||||
});
|
||||
|
||||
it("combatant with explicit side override is placed correctly", () => {
|
||||
const wrapper = makeWrapper({
|
||||
encounter: buildEncounter({
|
||||
combatants: [
|
||||
buildCombatant({
|
||||
id: combatantId("c-1"),
|
||||
name: "Hero",
|
||||
playerCharacterId: pcId1,
|
||||
}),
|
||||
buildCombatant({
|
||||
id: combatantId("c-2"),
|
||||
name: "Allied Guard",
|
||||
creatureId: goblinCreature.id,
|
||||
side: "party",
|
||||
}),
|
||||
buildCombatant({
|
||||
id: combatantId("c-3"),
|
||||
name: "Thug",
|
||||
cr: "1",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
playerCharacters: [
|
||||
{ id: pcId1, name: "Hero", ac: 15, maxHp: 30, level: 5 },
|
||||
],
|
||||
creatures: new Map([[goblinCreature.id, goblinCreature]]),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useDifficultyBreakdown(), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
const breakdown = result.current;
|
||||
expect(breakdown).not.toBeNull();
|
||||
// Allied Guard should be in party column
|
||||
expect(breakdown?.partyCombatants).toHaveLength(2);
|
||||
expect(breakdown?.partyCombatants[1].combatant.name).toBe("Allied Guard");
|
||||
expect(breakdown?.partyCombatants[1].side).toBe("party");
|
||||
// Thug in enemy column
|
||||
expect(breakdown?.enemyCombatants).toHaveLength(1);
|
||||
expect(breakdown?.enemyCombatants[0].combatant.name).toBe("Thug");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user