Files
initiative/apps/web/src/adapters/__tests__/bestiary-adapter.test.ts

334 lines
7.6 KiB
TypeScript

import { beforeAll, describe, expect, it } from "vitest";
import {
normalizeBestiary,
setSourceDisplayNames,
} from "../bestiary-adapter.js";
beforeAll(() => {
setSourceDisplayNames({ XMM: "MM 2024" });
});
describe("normalizeBestiary", () => {
it("normalizes a simple creature", () => {
const raw = {
monster: [
{
name: "Goblin Warrior",
source: "XMM",
size: ["S"],
type: { type: "fey", tags: ["goblinoid"] },
alignment: ["C", "N"],
ac: [15],
hp: { average: 10, formula: "3d6" },
speed: { walk: 30 },
str: 8,
dex: 15,
con: 10,
int: 10,
wis: 8,
cha: 8,
skill: { stealth: "+6" },
senses: ["Darkvision 60 ft."],
passive: 9,
languages: ["Common", "Goblin"],
cr: "1/4",
action: [
{
name: "Scimitar",
entries: [
"{@atkr m} {@hit 4}, reach 5 ft. {@h}5 ({@damage 1d6 + 2}) Slashing damage.",
],
},
],
bonus: [
{
name: "Nimble Escape",
entries: [
"The goblin takes the {@action Disengage|XPHB} or {@action Hide|XPHB} action.",
],
},
],
},
],
};
const creatures = normalizeBestiary(raw);
expect(creatures).toHaveLength(1);
const c = creatures[0];
expect(c.id).toBe("xmm:goblin-warrior");
expect(c.name).toBe("Goblin Warrior");
expect(c.source).toBe("XMM");
expect(c.sourceDisplayName).toBe("MM 2024");
expect(c.size).toBe("Small");
expect(c.type).toBe("Fey (Goblinoid)");
expect(c.alignment).toBe("Chaotic Neutral");
expect(c.ac).toBe(15);
expect(c.hp).toEqual({ average: 10, formula: "3d6" });
expect(c.speed).toBe("30 ft.");
expect(c.abilities.dex).toBe(15);
expect(c.cr).toBe("1/4");
expect(c.proficiencyBonus).toBe(2);
expect(c.passive).toBe(9);
expect(c.skills).toBe("Stealth +6");
expect(c.senses).toBe("Darkvision 60 ft.");
expect(c.languages).toBe("Common, Goblin");
expect(c.actions).toHaveLength(1);
expect(c.actions?.[0].text).toContain("Melee Attack Roll:");
expect(c.actions?.[0].text).not.toContain("{@");
expect(c.bonusActions).toHaveLength(1);
expect(c.bonusActions?.[0].text).toContain("Disengage");
expect(c.bonusActions?.[0].text).not.toContain("{@");
});
it("normalizes a creature with legendary actions", () => {
const raw = {
monster: [
{
name: "Aboleth",
source: "XMM",
size: ["L"],
type: "aberration",
alignment: ["L", "E"],
ac: [17],
hp: { average: 135, formula: "18d10 + 36" },
speed: { walk: 10, swim: 40 },
str: 21,
dex: 9,
con: 15,
int: 18,
wis: 15,
cha: 18,
save: { con: "+6", int: "+8", wis: "+6" },
senses: ["Darkvision 120 ft."],
passive: 12,
languages: ["Deep Speech", "Telepathy 120 ft."],
cr: "10",
legendary: [
{
name: "Lash",
entries: ["The aboleth makes one Tentacle attack."],
},
],
},
],
};
const creatures = normalizeBestiary(raw);
const c = creatures[0];
expect(c.legendaryActions).toBeDefined();
expect(c.legendaryActions?.preamble).toContain("3 Legendary Actions");
expect(c.legendaryActions?.entries).toHaveLength(1);
expect(c.legendaryActions?.entries[0].name).toBe("Lash");
});
it("normalizes a creature with spellcasting", () => {
const raw = {
monster: [
{
name: "Test Caster",
source: "XMM",
size: ["M"],
type: "humanoid",
ac: [12],
hp: { average: 40, formula: "9d8" },
speed: { walk: 30 },
str: 10,
dex: 14,
con: 10,
int: 17,
wis: 12,
cha: 11,
passive: 11,
cr: "6",
spellcasting: [
{
name: "Spellcasting",
headerEntries: [
"The caster casts spells using Intelligence (spell save {@dc 15}):",
],
will: ["{@spell Detect Magic|XPHB}", "{@spell Mage Hand|XPHB}"],
daily: {
"2e": ["{@spell Fireball|XPHB}"],
"1": ["{@spell Dimension Door|XPHB}"],
},
},
],
},
],
};
const creatures = normalizeBestiary(raw);
const c = creatures[0];
expect(c.spellcasting).toHaveLength(1);
const sc = c.spellcasting?.[0];
expect(sc).toBeDefined();
expect(sc?.name).toBe("Spellcasting");
expect(sc?.headerText).toContain("DC 15");
expect(sc?.headerText).not.toContain("{@");
expect(sc?.atWill).toEqual(["Detect Magic", "Mage Hand"]);
expect(sc?.daily).toHaveLength(2);
expect(sc?.daily).toContainEqual({
uses: 2,
each: true,
spells: ["Fireball"],
});
expect(sc?.daily).toContainEqual({
uses: 1,
each: false,
spells: ["Dimension Door"],
});
});
it("normalizes a creature with object-type type field", () => {
const raw = {
monster: [
{
name: "Swarm of Bats",
source: "XMM",
size: ["L"],
type: { type: "beast", swarmSize: "T" },
ac: [12],
hp: { average: 11, formula: "2d10" },
speed: { walk: 5, fly: 30 },
str: 5,
dex: 15,
con: 10,
int: 2,
wis: 12,
cha: 4,
passive: 11,
resist: ["bludgeoning", "piercing", "slashing"],
conditionImmune: ["charmed", "frightened"],
cr: "1/4",
},
],
};
const creatures = normalizeBestiary(raw);
const c = creatures[0];
expect(c.type).toBe("Swarm of Tiny Beasts");
expect(c.resist).toBe("Bludgeoning, Piercing, Slashing");
expect(c.conditionImmune).toBe("Charmed, Frightened");
});
it("normalizes a creature with conditional resistances", () => {
const raw = {
monster: [
{
name: "Half-Dragon",
source: "XMM",
size: ["M"],
type: "humanoid",
ac: [18],
hp: { average: 65, formula: "10d8 + 20" },
speed: { walk: 30 },
str: 16,
dex: 13,
con: 14,
int: 10,
wis: 11,
cha: 10,
passive: 10,
cr: "5",
resist: [
{
special: "Damage type chosen for the Draconic Origin trait",
},
],
},
],
};
const creatures = normalizeBestiary(raw);
const c = creatures[0];
expect(c.resist).toBe("Damage type chosen for the Draconic Origin trait");
});
it("normalizes a creature with multiple sizes", () => {
const raw = {
monster: [
{
name: "Aberrant Cultist",
source: "XMM",
size: ["S", "M"],
type: "humanoid",
ac: [13],
hp: { average: 22, formula: "4d8 + 4" },
speed: { walk: 30 },
str: 11,
dex: 14,
con: 12,
int: 10,
wis: 13,
cha: 8,
passive: 11,
cr: "1/2",
},
],
};
const creatures = normalizeBestiary(raw);
expect(creatures[0].size).toBe("Small or Medium");
});
it("normalizes a creature with CR as object", () => {
const raw = {
monster: [
{
name: "Dragon",
source: "XMM",
size: ["H"],
type: "dragon",
ac: [19],
hp: { average: 256, formula: "19d12 + 133" },
speed: { walk: 40 },
str: 27,
dex: 10,
con: 25,
int: 16,
wis: 13,
cha: 23,
passive: 23,
cr: { cr: "17", xpLair: 20000 },
},
],
};
const creatures = normalizeBestiary(raw);
expect(creatures[0].cr).toBe("17");
expect(creatures[0].proficiencyBonus).toBe(6);
});
it("handles fly speed with hover condition", () => {
const raw = {
monster: [
{
name: "Air Elemental",
source: "XMM",
size: ["L"],
type: "elemental",
ac: [15],
hp: { average: 90, formula: "12d10 + 24" },
speed: {
walk: 10,
fly: { number: 90, condition: "(hover)" },
canHover: true,
},
str: 14,
dex: 20,
con: 14,
int: 6,
wis: 10,
cha: 6,
passive: 10,
cr: "5",
},
],
};
const creatures = normalizeBestiary(raw);
expect(creatures[0].speed).toBe("10 ft., fly 90 ft. (hover)");
});
});