From f9cfaa25705c8bd5b637c8c54a0c3be6ddd115de Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 7 Apr 2026 16:29:08 +0200 Subject: [PATCH] Include traits on PF2e ability blocks Parse and display traits (concentrate, divine, polymorph, etc.) on ability entries, matching how attack traits are already shown. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../__tests__/pf2e-bestiary-adapter.test.ts | 62 +++++++++++++++++++ .../web/src/adapters/pf2e-bestiary-adapter.ts | 23 ++++++- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/apps/web/src/adapters/__tests__/pf2e-bestiary-adapter.test.ts b/apps/web/src/adapters/__tests__/pf2e-bestiary-adapter.test.ts index e2cc9ec..925747e 100644 --- a/apps/web/src/adapters/__tests__/pf2e-bestiary-adapter.test.ts +++ b/apps/web/src/adapters/__tests__/pf2e-bestiary-adapter.test.ts @@ -170,6 +170,68 @@ describe("normalizePf2eBestiary", () => { }); }); + describe("ability traits formatting", () => { + it("includes traits from abilities in the text", () => { + const [creature] = normalizePf2eBestiary({ + creature: [ + minimalCreature({ + abilities: { + bot: [ + { + name: "Change Shape", + traits: [ + "concentrate", + "divine", + "polymorph", + "transmutation", + ], + entries: [ + "The naunet can take the appearance of any creature.", + ], + }, + ], + }, + }), + ], + }); + const ability = creature.abilitiesBot?.[0]; + expect(ability).toBeDefined(); + expect(ability?.segments[0]).toEqual( + expect.objectContaining({ + type: "text", + value: expect.stringContaining( + "(Concentrate, Divine, Polymorph, Transmutation)", + ), + }), + ); + }); + + it("renders ability without traits normally", () => { + const [creature] = normalizePf2eBestiary({ + creature: [ + minimalCreature({ + abilities: { + bot: [ + { + name: "Constrict", + entries: ["1d8+8 bludgeoning, DC 26"], + }, + ], + }, + }), + ], + }); + const ability = creature.abilitiesBot?.[0]; + expect(ability).toBeDefined(); + expect(ability?.segments[0]).toEqual( + expect.objectContaining({ + type: "text", + value: "1d8+8 bludgeoning, DC 26", + }), + ); + }); + }); + describe("resistances formatting", () => { it("formats resistance without amount", () => { const [creature] = normalizePf2eBestiary({ diff --git a/apps/web/src/adapters/pf2e-bestiary-adapter.ts b/apps/web/src/adapters/pf2e-bestiary-adapter.ts index d2281de..9f73fc1 100644 --- a/apps/web/src/adapters/pf2e-bestiary-adapter.ts +++ b/apps/web/src/adapters/pf2e-bestiary-adapter.ts @@ -46,6 +46,7 @@ interface RawDefenses { interface RawAbility { name?: string; + traits?: string[]; entries?: RawEntry[]; } @@ -244,11 +245,27 @@ function normalizeAbilities( .filter((a) => a.name) .map((a) => { const raw = a as Record; + const traits = + a.traits && a.traits.length > 0 + ? `(${a.traits.map((t) => capitalize(stripAngleBrackets(stripTags(t)))).join(", ")}) ` + : ""; + const body = Array.isArray(a.entries) + ? segmentizeEntries(a.entries) + : formatAffliction(raw); + if (traits && body.length > 0 && body[0].type === "text") { + return { + name: stripTags(a.name as string), + segments: [ + { type: "text" as const, value: traits + body[0].value }, + ...body.slice(1), + ], + }; + } return { name: stripTags(a.name as string), - segments: Array.isArray(a.entries) - ? segmentizeEntries(a.entries) - : formatAffliction(raw), + segments: traits + ? [{ type: "text" as const, value: traits }, ...body] + : body, }; }); }