Add PF2e weak/elite creature adjustments with stat block toggle
All checks were successful
CI / check (push) Successful in 2m32s
CI / build-image (push) Successful in 19s

Weak/Normal/Elite toggle in PF2e stat block header applies standard
adjustments (level, AC, HP, saves, Perception, attacks, damage) to
individual combatants. Adjusted stats are highlighted blue (elite) or
red (weak). Persisted via creatureAdjustment field on Combatant.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-04-11 02:24:30 +02:00
parent a44f82127e
commit 09a801487d
18 changed files with 985 additions and 31 deletions

View File

@@ -0,0 +1,270 @@
import { describe, expect, it } from "vitest";
import type { Pf2eCreature } from "../creature-types.js";
import { creatureId } from "../creature-types.js";
import {
acDelta,
adjustedLevel,
applyPf2eAdjustment,
hpDelta,
modDelta,
} from "../pf2e-adjustments.js";
describe("adjustedLevel", () => {
it("elite on level 5 → 6", () => {
expect(adjustedLevel(5, "elite")).toBe(6);
});
it("elite on level 0 → 2 (double bump)", () => {
expect(adjustedLevel(0, "elite")).toBe(2);
});
it("elite on level 1 → 1 (double bump)", () => {
expect(adjustedLevel(-1, "elite")).toBe(1);
});
it("weak on level 5 → 4", () => {
expect(adjustedLevel(5, "weak")).toBe(4);
});
it("weak on level 1 → 1 (double drop)", () => {
expect(adjustedLevel(1, "weak")).toBe(-1);
});
it("weak on level 0 → 1", () => {
expect(adjustedLevel(0, "weak")).toBe(-1);
});
});
describe("hpDelta", () => {
it("level 1 elite → +10", () => {
expect(hpDelta(1, "elite")).toBe(10);
});
it("level 1 weak → 10", () => {
expect(hpDelta(1, "weak")).toBe(-10);
});
it("level 3 elite → +15", () => {
expect(hpDelta(3, "elite")).toBe(15);
});
it("level 3 weak → 15", () => {
expect(hpDelta(3, "weak")).toBe(-15);
});
it("level 10 elite → +20", () => {
expect(hpDelta(10, "elite")).toBe(20);
});
it("level 10 weak → 20", () => {
expect(hpDelta(10, "weak")).toBe(-20);
});
it("level 25 elite → +30", () => {
expect(hpDelta(25, "elite")).toBe(30);
});
it("level 25 weak → 30", () => {
expect(hpDelta(25, "weak")).toBe(-30);
});
});
describe("acDelta", () => {
it("elite → +2", () => {
expect(acDelta("elite")).toBe(2);
});
it("weak → 2", () => {
expect(acDelta("weak")).toBe(-2);
});
});
describe("modDelta", () => {
it("elite → +2", () => {
expect(modDelta("elite")).toBe(2);
});
it("weak → 2", () => {
expect(modDelta("weak")).toBe(-2);
});
});
function baseCreature(overrides?: Partial<Pf2eCreature>): Pf2eCreature {
return {
system: "pf2e",
id: creatureId("test-creature"),
name: "Test Creature",
source: "test-source",
sourceDisplayName: "Test Source",
level: 5,
traits: ["humanoid"],
perception: 12,
skills: "Athletics +14",
abilityMods: {
str: 4,
dex: 2,
con: 3,
int: 0,
wis: 1,
cha: -1,
},
ac: 22,
saveFort: 14,
saveRef: 11,
saveWill: 9,
hp: 75,
speed: "25 feet",
...overrides,
};
}
describe("applyPf2eAdjustment", () => {
it("adjusts all numeric stats for elite", () => {
const creature = baseCreature();
const result = applyPf2eAdjustment(creature, "elite");
expect(result.level).toBe(6);
expect(result.ac).toBe(24);
expect(result.hp).toBe(95); // 75 + 20 (level 5 bracket)
expect(result.perception).toBe(14);
expect(result.saveFort).toBe(16);
expect(result.saveRef).toBe(13);
expect(result.saveWill).toBe(11);
});
it("adjusts all numeric stats for weak", () => {
const creature = baseCreature();
const result = applyPf2eAdjustment(creature, "weak");
expect(result.level).toBe(4);
expect(result.ac).toBe(20);
expect(result.hp).toBe(55); // 75 - 20 (level 5 bracket)
expect(result.perception).toBe(10);
expect(result.saveFort).toBe(12);
expect(result.saveRef).toBe(9);
expect(result.saveWill).toBe(7);
});
it("adjusts attack bonuses and damage", () => {
const creature = baseCreature({
attacks: [
{
name: "Melee",
activity: { number: 1, unit: "action" },
segments: [
{
type: "text",
value: "+15 [+10/+5] (agile), 2d12+7 piercing plus Grab",
},
],
},
],
});
const result = applyPf2eAdjustment(creature, "elite");
const text = result.attacks?.[0].segments[0];
expect(text).toEqual({
type: "text",
value: "+17 [+12/+7] (agile), 2d12+9 piercing plus Grab",
});
});
it("adjusts attack damage for weak", () => {
const creature = baseCreature({
attacks: [
{
name: "Melee",
activity: { number: 1, unit: "action" },
segments: [
{
type: "text",
value: "+15 (agile), 2d12+7 piercing plus Grab",
},
],
},
],
});
const result = applyPf2eAdjustment(creature, "weak");
const text = result.attacks?.[0].segments[0];
expect(text).toEqual({
type: "text",
value: "+13 (agile), 2d12+5 piercing plus Grab",
});
});
it("handles damage bonus becoming zero", () => {
const creature = baseCreature({
attacks: [
{
name: "Melee",
activity: { number: 1, unit: "action" },
segments: [{ type: "text", value: "+10, 1d4+2 slashing" }],
},
],
});
const result = applyPf2eAdjustment(creature, "weak");
const text = result.attacks?.[0].segments[0];
expect(text).toEqual({
type: "text",
value: "+8, 1d4 slashing",
});
});
it("handles damage bonus becoming negative", () => {
const creature = baseCreature({
attacks: [
{
name: "Melee",
activity: { number: 1, unit: "action" },
segments: [{ type: "text", value: "+10, 1d4+1 slashing" }],
},
],
});
const result = applyPf2eAdjustment(creature, "weak");
const text = result.attacks?.[0].segments[0];
expect(text).toEqual({
type: "text",
value: "+8, 1d4-1 slashing",
});
});
it("does not modify non-attack abilities", () => {
const creature = baseCreature({
abilitiesTop: [
{
name: "Darkvision",
segments: [{ type: "text", value: "Can see in darkness." }],
},
],
});
const result = applyPf2eAdjustment(creature, "elite");
expect(result.abilitiesTop).toEqual(creature.abilitiesTop);
});
it("preserves non-text segments in attacks", () => {
const creature = baseCreature({
attacks: [
{
name: "Melee",
activity: { number: 1, unit: "action" },
segments: [
{
type: "list",
items: [{ text: "some list item" }],
},
],
},
],
});
const result = applyPf2eAdjustment(creature, "elite");
expect(result.attacks?.[0].segments[0]).toEqual({
type: "list",
items: [{ text: "some list item" }],
});
});
});