From 8dbff66ce1eb37ce521402f10e162c99e918df5f Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 7 Apr 2026 12:06:22 +0200 Subject: [PATCH] Fix "undefined" in PF2e stat block weaknesses/resistances Some PF2e creatures (e.g. Giant Mining Bee) have qualitative weaknesses without a numeric amount, causing "undefined" to render in the stat block. Handle missing amounts gracefully. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../__tests__/pf2e-bestiary-adapter.test.ts | 91 +++++++++++++++++++ .../web/src/adapters/pf2e-bestiary-adapter.ts | 32 ++++--- 2 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 apps/web/src/adapters/__tests__/pf2e-bestiary-adapter.test.ts diff --git a/apps/web/src/adapters/__tests__/pf2e-bestiary-adapter.test.ts b/apps/web/src/adapters/__tests__/pf2e-bestiary-adapter.test.ts new file mode 100644 index 0000000..3b1e718 --- /dev/null +++ b/apps/web/src/adapters/__tests__/pf2e-bestiary-adapter.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from "vitest"; +import { normalizePf2eBestiary } from "../pf2e-bestiary-adapter.js"; + +function minimalCreature(defenses?: Record) { + return { + name: "Test Creature", + source: "TST", + defenses, + }; +} + +describe("normalizePf2eBestiary", () => { + describe("weaknesses formatting", () => { + it("formats weakness with numeric amount", () => { + const [creature] = normalizePf2eBestiary({ + creature: [ + minimalCreature({ + weaknesses: [{ name: "fire", amount: 5 }], + }), + ], + }); + expect(creature.weaknesses).toBe("Fire 5"); + }); + + it("formats weakness without amount (qualitative)", () => { + const [creature] = normalizePf2eBestiary({ + creature: [ + minimalCreature({ + weaknesses: [{ name: "smoke susceptibility" }], + }), + ], + }); + expect(creature.weaknesses).toBe("Smoke susceptibility"); + }); + + it("formats weakness with note and amount", () => { + const [creature] = normalizePf2eBestiary({ + creature: [ + minimalCreature({ + weaknesses: [ + { name: "cold iron", amount: 5, note: "except daggers" }, + ], + }), + ], + }); + expect(creature.weaknesses).toBe("Cold iron 5 (except daggers)"); + }); + + it("formats weakness with note but no amount", () => { + const [creature] = normalizePf2eBestiary({ + creature: [ + minimalCreature({ + weaknesses: [{ name: "smoke susceptibility", note: "see below" }], + }), + ], + }); + expect(creature.weaknesses).toBe("Smoke susceptibility (see below)"); + }); + + it("returns undefined when no weaknesses", () => { + const [creature] = normalizePf2eBestiary({ + creature: [minimalCreature({})], + }); + expect(creature.weaknesses).toBeUndefined(); + }); + }); + + describe("resistances formatting", () => { + it("formats resistance without amount", () => { + const [creature] = normalizePf2eBestiary({ + creature: [ + minimalCreature({ + resistances: [{ name: "physical" }], + }), + ], + }); + expect(creature.resistances).toBe("Physical"); + }); + + it("formats resistance with amount", () => { + const [creature] = normalizePf2eBestiary({ + creature: [ + minimalCreature({ + resistances: [{ name: "fire", amount: 10 }], + }), + ], + }); + expect(creature.resistances).toBe("Fire 10"); + }); + }); +}); diff --git a/apps/web/src/adapters/pf2e-bestiary-adapter.ts b/apps/web/src/adapters/pf2e-bestiary-adapter.ts index a690a7c..4776d91 100644 --- a/apps/web/src/adapters/pf2e-bestiary-adapter.ts +++ b/apps/web/src/adapters/pf2e-bestiary-adapter.ts @@ -40,8 +40,8 @@ interface RawDefenses { }; hp?: { hp?: number }[]; immunities?: (string | { name: string })[]; - resistances?: { amount: number; name: string; note?: string }[]; - weaknesses?: { amount: number; name: string; note?: string }[]; + resistances?: { amount?: number; name: string; note?: string }[]; + weaknesses?: { amount?: number; name: string; note?: string }[]; } interface RawAbility { @@ -150,31 +150,35 @@ function formatImmunities( function formatResistances( resistances: - | readonly { amount: number; name: string; note?: string }[] + | readonly { amount?: number; name: string; note?: string }[] | undefined, ): string | undefined { if (!resistances || resistances.length === 0) return undefined; return resistances - .map((r) => - r.note - ? `${capitalize(r.name)} ${r.amount} (${r.note})` - : `${capitalize(r.name)} ${r.amount}`, - ) + .map((r) => { + const base = + r.amount == null + ? capitalize(r.name) + : `${capitalize(r.name)} ${r.amount}`; + return r.note ? `${base} (${r.note})` : base; + }) .join(", "); } function formatWeaknesses( weaknesses: - | readonly { amount: number; name: string; note?: string }[] + | readonly { amount?: number; name: string; note?: string }[] | undefined, ): string | undefined { if (!weaknesses || weaknesses.length === 0) return undefined; return weaknesses - .map((w) => - w.note - ? `${capitalize(w.name)} ${w.amount} (${w.note})` - : `${capitalize(w.name)} ${w.amount}`, - ) + .map((w) => { + const base = + w.amount == null + ? capitalize(w.name) + : `${capitalize(w.name)} ${w.amount}`; + return w.note ? `${base} (${w.note})` : base; + }) .join(", "); }