Add PF2e equipment display with detail popovers in stat blocks
All checks were successful
CI / check (push) Successful in 2m31s
CI / build-image (push) Successful in 17s

Extract shared DetailPopover shell from spell popovers. Normalize
weapon/consumable/equipment/armor items from Foundry data into
mundane (Items line) and detailed (Equipment section with clickable
popovers). Scrolls/wands show embedded spell info. Bump IDB cache v7.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-04-10 20:21:11 +02:00
parent e2e8297c95
commit 1eaeecad32
16 changed files with 943 additions and 158 deletions

View File

@@ -541,6 +541,288 @@ describe("normalizeFoundryCreature", () => {
});
});
describe("equipment normalization", () => {
it("normalizes a weapon with traits and description", () => {
const creature = normalizeFoundryCreature(
minimalCreature({
items: [
{
_id: "w1",
name: "Flaming Longsword",
type: "weapon",
system: {
level: { value: 5 },
traits: { value: ["magical", "fire"] },
description: {
value: "<p>This sword blazes with fire.</p>",
},
},
},
],
}),
);
expect(creature.equipment).toHaveLength(1);
const item = creature.equipment?.[0];
expect(item?.name).toBe("Flaming Longsword");
expect(item?.level).toBe(5);
expect(item?.traits).toEqual(["magical", "fire"]);
expect(item?.description).toBe("This sword blazes with fire.");
});
it("normalizes a consumable potion with description", () => {
const creature = normalizeFoundryCreature(
minimalCreature({
items: [
{
_id: "c1",
name: "Healing Potion (Moderate)",
type: "consumable",
system: {
level: { value: 6 },
traits: { value: ["consumable", "healing", "magical"] },
description: {
value: "<p>Restores 3d8+10 Hit Points.</p>",
},
category: "potion",
},
},
],
}),
);
expect(creature.equipment).toHaveLength(1);
const item = creature.equipment?.[0];
expect(item?.name).toBe("Healing Potion (Moderate)");
expect(item?.category).toBe("potion");
expect(item?.description).toBe("Restores 3d8+10 Hit Points.");
});
it("extracts scroll embedded spell name and rank", () => {
const creature = normalizeFoundryCreature(
minimalCreature({
items: [
{
_id: "s1",
name: "Scroll of Teleport (Rank 6)",
type: "consumable",
system: {
level: { value: 11 },
traits: { value: ["consumable", "magical", "scroll"] },
description: { value: "<p>A scroll.</p>" },
category: "scroll",
spell: {
name: "Teleport",
system: { level: { value: 6 } },
},
},
},
],
}),
);
const item = creature.equipment?.[0];
expect(item?.spellName).toBe("Teleport");
expect(item?.spellRank).toBe(6);
});
it("extracts wand embedded spell name and rank", () => {
const creature = normalizeFoundryCreature(
minimalCreature({
items: [
{
_id: "w1",
name: "Wand of Dispel Magic (Rank 2)",
type: "consumable",
system: {
level: { value: 5 },
traits: { value: ["consumable", "magical", "wand"] },
description: { value: "<p>A wand.</p>" },
category: "wand",
spell: {
name: "Dispel Magic",
system: { level: { value: 2 } },
},
},
},
],
}),
);
const item = creature.equipment?.[0];
expect(item?.spellName).toBe("Dispel Magic");
expect(item?.spellRank).toBe(2);
});
it("filters magical equipment into equipment field", () => {
const creature = normalizeFoundryCreature(
minimalCreature({
items: [
{
_id: "e1",
name: "Ring of Energy Resistance (Fire)",
type: "equipment",
system: {
level: { value: 6 },
traits: { value: ["magical", "invested"] },
description: {
value: "<p>Grants fire resistance 5.</p>",
},
},
},
],
}),
);
expect(creature.equipment).toHaveLength(1);
expect(creature.equipment?.[0]?.name).toBe(
"Ring of Energy Resistance (Fire)",
);
expect(creature.items).toBeUndefined();
});
it("filters mundane items into items string", () => {
const creature = normalizeFoundryCreature(
minimalCreature({
items: [
{
_id: "w1",
name: "Longsword",
type: "weapon",
system: {
level: { value: 0 },
traits: { value: [] },
description: { value: "" },
},
},
{
_id: "a1",
name: "Leather Armor",
type: "armor",
system: {
level: { value: 0 },
traits: { value: [] },
description: { value: "" },
},
},
],
}),
);
expect(creature.items).toBe("Longsword, Leather Armor");
expect(creature.equipment).toBeUndefined();
});
it("omits equipment when no detailed items exist", () => {
const creature = normalizeFoundryCreature(
minimalCreature({
items: [
{
_id: "w1",
name: "Dagger",
type: "weapon",
system: {
level: { value: 0 },
traits: { value: [] },
description: { value: "" },
},
},
],
}),
);
expect(creature.equipment).toBeUndefined();
});
it("omits items when no mundane items exist", () => {
const creature = normalizeFoundryCreature(
minimalCreature({
items: [
{
_id: "c1",
name: "Giant Wasp Venom",
type: "consumable",
system: {
level: { value: 7 },
traits: { value: ["consumable", "poison"] },
description: {
value: "<p>A deadly poison.</p>",
},
category: "poison",
},
},
],
}),
);
expect(creature.items).toBeUndefined();
expect(creature.equipment).toHaveLength(1);
});
it("includes armor with special material in equipment", () => {
const creature = normalizeFoundryCreature(
minimalCreature({
items: [
{
_id: "a1",
name: "Adamantine Full Plate",
type: "armor",
system: {
level: { value: 0 },
traits: { value: [] },
description: {
value: "<p>Full plate made of adamantine.</p>",
},
material: { type: "adamantine", grade: "standard" },
},
},
],
}),
);
expect(creature.equipment).toHaveLength(1);
expect(creature.equipment?.[0]?.name).toBe("Adamantine Full Plate");
});
it("excludes mundane armor from equipment (goes to items)", () => {
const creature = normalizeFoundryCreature(
minimalCreature({
items: [
{
_id: "a1",
name: "Chain Mail",
type: "armor",
system: {
level: { value: 0 },
traits: { value: [] },
description: { value: "" },
},
},
],
}),
);
expect(creature.equipment).toBeUndefined();
expect(creature.items).toBe("Chain Mail");
});
it("strips Foundry HTML tags from equipment descriptions", () => {
const creature = normalizeFoundryCreature(
minimalCreature({
items: [
{
_id: "c1",
name: "Potion of Speed",
type: "consumable",
system: {
level: { value: 10 },
traits: { value: ["consumable", "magical"] },
description: {
value:
"<p>Gain @UUID[Compendium.pf2e.conditionitems.Item.Quickened]{quickened} for 1 minute.</p>",
},
category: "potion",
},
},
],
}),
);
const desc = creature.equipment?.[0]?.description;
expect(desc).toBe("Gain quickened for 1 minute.");
expect(desc).not.toContain("@UUID");
});
});
describe("spellcasting normalization", () => {
it("normalizes prepared spells by rank", () => {
const creature = normalizeFoundryCreature(
@@ -597,7 +879,8 @@ describe("normalizeFoundryCreature", () => {
expect(sc?.daily?.[0]?.spells.map((s) => s.name)).toEqual(["Earthquake"]);
expect(sc?.daily?.[1]?.spells.map((s) => s.name)).toEqual(["Heal"]);
expect(sc?.atWill?.map((s) => s.name)).toEqual(["Detect Magic"]);
expect(sc?.atWill?.[0]?.rank).toBe(1);
// Cantrip rank auto-heightens to ceil(creatureLevel / 2) = ceil(3/2) = 2
expect(sc?.atWill?.[0]?.rank).toBe(2);
});
it("normalizes innate spells with uses", () => {

View File

@@ -99,9 +99,15 @@ describe("stripFoundryTags", () => {
expect(stripFoundryTags("before<hr />after")).toBe("before\nafter");
});
it("strips strong and em tags", () => {
it("preserves strong and em tags", () => {
expect(stripFoundryTags("<strong>bold</strong> <em>italic</em>")).toBe(
"bold italic",
"<strong>bold</strong> <em>italic</em>",
);
});
it("preserves list tags", () => {
expect(stripFoundryTags("<ul><li>first</li><li>second</li></ul>")).toBe(
"<ul><li>first</li><li>second</li></ul>",
);
});