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>
271 lines
5.9 KiB
TypeScript
271 lines
5.9 KiB
TypeScript
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" }],
|
||
});
|
||
});
|
||
});
|