3e5bdc06e2
- Condition durations + auto-expiry: timed conditions tick down on the affected combatant's turn and drop at 0 (engine tickConditions; UI rounds field + (Nr) chip) - Initiative: Roll-all (d20 + per-combatant bonus, re-sort anchored) + group/quantity add - Combat log: per-round event feed (turns, round changes, damage/heal, expiries, removals) + in-memory Undo of the last change - Encounter difficulty budget: 5e XP thresholds + PF2e level budget (pure lib + tests); live difficulty/XP-award readout from monster combatants vs the party - Combatants carry initBonus + cr/level; compendium add-to-combat passes them 18 new unit tests (durations, applyInitiatives, budget), new combat-depth e2e. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
46 lines
1.7 KiB
TypeScript
46 lines
1.7 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { computeBudget } from './budget';
|
|
|
|
describe('5e encounter budget', () => {
|
|
it('rates a single CR 1/4 vs a level-1 party of 4 as easy-ish', () => {
|
|
const r = computeBudget('5e', [1, 1, 1, 1], [{ cr: 0.25 }]);
|
|
expect(r.totalXp).toBe(50);
|
|
expect(r.ratingXp).toBe(50); // single monster, x1 multiplier
|
|
expect(['trivial', 'easy']).toContain(r.difficulty);
|
|
});
|
|
|
|
it('applies the count multiplier to the rating but not the award', () => {
|
|
const r = computeBudget('5e', [3, 3, 3, 3], [{ cr: 1 }, { cr: 1 }, { cr: 1 }]);
|
|
expect(r.totalXp).toBe(600); // 3 * 200
|
|
expect(r.ratingXp).toBe(1200); // x2 for 3 monsters
|
|
expect(r.awardPerCharacter).toBe(150); // 600 / 4
|
|
});
|
|
|
|
it('flags a deadly fight', () => {
|
|
const r = computeBudget('5e', [1, 1, 1, 1], [{ cr: 3 }, { cr: 3 }]);
|
|
expect(r.difficulty).toBe('deadly');
|
|
});
|
|
});
|
|
|
|
describe('pf2e encounter budget', () => {
|
|
it('a same-level creature is worth 40 XP', () => {
|
|
const r = computeBudget('pf2e', [3, 3, 3, 3], [{ level: 3 }]);
|
|
expect(r.totalXp).toBe(40);
|
|
expect(r.awardPerCharacter).toBe(40);
|
|
});
|
|
|
|
it('rates a moderate encounter', () => {
|
|
const r = computeBudget('pf2e', [5, 5, 5, 5], [{ level: 5 }, { level: 5 }]);
|
|
expect(r.totalXp).toBe(80);
|
|
expect(r.difficulty).toBe('moderate');
|
|
});
|
|
|
|
it('adjusts thresholds for party size', () => {
|
|
const four = computeBudget('pf2e', [1, 1, 1, 1], []);
|
|
const five = computeBudget('pf2e', [1, 1, 1, 1, 1], []);
|
|
const sevFour = four.thresholds.find((t) => t.label === 'Severe')!.value;
|
|
const sevFive = five.thresholds.find((t) => t.label === 'Severe')!.value;
|
|
expect(sevFive - sevFour).toBe(30);
|
|
});
|
|
});
|