Fix PF2e rules math: AC proficiency, weapon damage, MAP, striking, slots, Refocus
Confirmed accuracy defects from the audit: - AC now scales with level + armor proficiency (10 + Dex + (level+rank) + item), defaulting to trained; was 10 + Dex only, wrong by +3..+28. - Weapon potency (itemBonus) applies to the attack roll ONLY, not flat damage. - weaponAttack now reports the Multiple Attack Penalty (-5/-10, agile -4/-8) and expands striking runes into extra weapon dice; AttacksSection surfaces MAP. - Full-caster spell slots: 2 at the rank's unlock level, 3 thereafter (was a flat 3, giving a level-1 caster 3 first-rank slots instead of 2). - PF2e Refocus restores one Focus Point (recoverStep), not the whole pool. Adds 6 regression tests. types.ts gains WeaponInput.agile/striking, WeaponResult.map, CharacterRulesInput.acRank, RestOption.recoverStep. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,8 @@ import { describe, it, expect } from 'vitest';
|
||||
import { abilityModifier } from './abilities';
|
||||
import { dnd5e, proficiencyBonus } from './dnd5e';
|
||||
import { pf2e, pf2eProficiency } from './pf2e';
|
||||
import { pf2eSlots } from './pf2e/progression';
|
||||
import { applyRest } from './rest';
|
||||
import type { CharacterRulesInput } from './types';
|
||||
|
||||
describe('abilityModifier', () => {
|
||||
@@ -100,4 +102,59 @@ describe('pf2e proficiency', () => {
|
||||
expect(saves.dex).toBe(2 + (4 + 2));
|
||||
expect(saves.str).toBe(0); // not a save: just ability mod
|
||||
});
|
||||
|
||||
it('AC scales with level + armor proficiency (not just 10 + Dex)', () => {
|
||||
// level-5 fighter, Dex +1, trained in defense, unarmored: 10 + 1 + (5 + 2) = 18
|
||||
const ac = pf2e.baseArmorClass({
|
||||
level: 5,
|
||||
abilities: { str: 18, dex: 12, con: 14, int: 10, wis: 12, cha: 10 },
|
||||
acRank: 'trained',
|
||||
});
|
||||
expect(ac).toBe(18);
|
||||
});
|
||||
|
||||
it('weapon attack: potency adds to-hit only, never flat damage', () => {
|
||||
// level-3, STR 18 (+4), trained (3+2=+5), +1 potency, 1d8
|
||||
const r = pf2e.weaponAttack(
|
||||
{ level: 3, abilities: { str: 18, dex: 10, con: 10, int: 10, wis: 10, cha: 10 } },
|
||||
{ ability: 'str', rank: 'trained', itemBonus: 1, damageDice: '1d8' },
|
||||
);
|
||||
expect(r.toHit).toBe(4 + 5 + 1); // +10
|
||||
expect(r.damage).toBe('1d8+4'); // STR only — no +item on damage
|
||||
});
|
||||
|
||||
it('weapon attack reports MAP (-5/-10, agile -4/-8) and striking dice', () => {
|
||||
const base = { level: 3, abilities: { str: 18, dex: 10, con: 10, int: 10, wis: 10, cha: 10 } };
|
||||
const sword = pf2e.weaponAttack(base, { ability: 'str', rank: 'trained', damageDice: '1d8', striking: 2 });
|
||||
expect(sword.toHit).toBe(9);
|
||||
expect(sword.map).toEqual({ second: 4, third: -1 }); // -5 / -10
|
||||
expect(sword.damage).toBe('2d8+4'); // striking doubles the dice
|
||||
const agile = pf2e.weaponAttack(base, { ability: 'str', rank: 'trained', damageDice: '1d6', agile: true });
|
||||
expect(agile.map).toEqual({ second: 5, third: 1 }); // -4 / -8
|
||||
});
|
||||
|
||||
it('full caster has 2 first-rank slots at level 1, 3 from level 2', () => {
|
||||
expect(pf2eSlots('full', 1)).toEqual([{ level: 1, max: 2, current: 2 }]);
|
||||
expect(pf2eSlots('full', 2)).toEqual([{ level: 1, max: 3, current: 3 }]);
|
||||
// level 3: 3 first-rank + 2 (newly unlocked) second-rank
|
||||
expect(pf2eSlots('full', 3)).toEqual([
|
||||
{ level: 1, max: 3, current: 3 },
|
||||
{ level: 2, max: 2, current: 2 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pf2e Refocus', () => {
|
||||
it('restores one Focus Point, not the whole pool', () => {
|
||||
const refocus = pf2e.restOptions.find((o) => o.id === 'refocus')!;
|
||||
const c = {
|
||||
resources: [{ id: 'f', name: 'Focus Points', current: 0, max: 3, recovery: 'short' as const }],
|
||||
hp: { current: 10, max: 10, temp: 0 },
|
||||
concentration: null,
|
||||
spellcasting: { slots: [], spells: [] },
|
||||
defenses: { exhaustion: 0, deathSaves: { successes: 0, failures: 0 } },
|
||||
} as unknown as Parameters<typeof applyRest>[0];
|
||||
const patch = applyRest(c, refocus);
|
||||
expect(patch.resources?.[0]?.current).toBe(1); // +1, clamped to max — not 3
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user