Expand pre-2024 {@atk} tags to full attack type labels in stat blocks
Old 5etools data uses {@atk mw} instead of {@atkr m}, which the generic
tag handler was reducing to bare "mw" text. Adds dedicated handling for
all {@atk} variants and bumps the bestiary cache version to clear stale
processed data.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -300,6 +300,44 @@ describe("normalizeBestiary", () => {
|
|||||||
expect(creatures[0].proficiencyBonus).toBe(6);
|
expect(creatures[0].proficiencyBonus).toBe(6);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("normalizes pre-2024 {@atk mw} tags to full attack type text", () => {
|
||||||
|
const raw = {
|
||||||
|
monster: [
|
||||||
|
{
|
||||||
|
name: "Adult Black Dragon",
|
||||||
|
source: "MM",
|
||||||
|
size: ["H"],
|
||||||
|
type: "dragon",
|
||||||
|
ac: [19],
|
||||||
|
hp: { average: 195, formula: "17d12 + 85" },
|
||||||
|
speed: { walk: 40, fly: 80, swim: 40 },
|
||||||
|
str: 23,
|
||||||
|
dex: 14,
|
||||||
|
con: 21,
|
||||||
|
int: 14,
|
||||||
|
wis: 13,
|
||||||
|
cha: 17,
|
||||||
|
passive: 21,
|
||||||
|
cr: "14",
|
||||||
|
action: [
|
||||||
|
{
|
||||||
|
name: "Bite",
|
||||||
|
entries: [
|
||||||
|
"{@atk mw} {@hit 11} to hit, reach 10 ft., one target. {@h}17 ({@damage 2d10 + 6}) piercing damage plus 4 ({@damage 1d8}) acid damage.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const creatures = normalizeBestiary(raw);
|
||||||
|
const bite = creatures[0].actions?.[0];
|
||||||
|
expect(bite?.text).toContain("Melee Weapon Attack:");
|
||||||
|
expect(bite?.text).not.toContain("mw");
|
||||||
|
expect(bite?.text).not.toContain("{@");
|
||||||
|
});
|
||||||
|
|
||||||
it("handles fly speed with hover condition", () => {
|
it("handles fly speed with hover condition", () => {
|
||||||
const raw = {
|
const raw = {
|
||||||
monster: [
|
monster: [
|
||||||
|
|||||||
@@ -50,6 +50,26 @@ describe("stripTags", () => {
|
|||||||
expect(stripTags("{@atkr m,r}")).toBe("Melee or Ranged Attack Roll:");
|
expect(stripTags("{@atkr m,r}")).toBe("Melee or Ranged Attack Roll:");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("strips {@atk mw} to Melee Weapon Attack:", () => {
|
||||||
|
expect(stripTags("{@atk mw}")).toBe("Melee Weapon Attack:");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips {@atk rw} to Ranged Weapon Attack:", () => {
|
||||||
|
expect(stripTags("{@atk rw}")).toBe("Ranged Weapon Attack:");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips {@atk ms} to Melee Spell Attack:", () => {
|
||||||
|
expect(stripTags("{@atk ms}")).toBe("Melee Spell Attack:");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips {@atk rs} to Ranged Spell Attack:", () => {
|
||||||
|
expect(stripTags("{@atk rs}")).toBe("Ranged Spell Attack:");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips {@atk mw,rw} to Melee or Ranged Weapon Attack:", () => {
|
||||||
|
expect(stripTags("{@atk mw,rw}")).toBe("Melee or Ranged Weapon Attack:");
|
||||||
|
});
|
||||||
|
|
||||||
it("strips {@recharge 5} to (Recharge 5-6)", () => {
|
it("strips {@recharge 5} to (Recharge 5-6)", () => {
|
||||||
expect(stripTags("{@recharge 5}")).toBe("(Recharge 5-6)");
|
expect(stripTags("{@recharge 5}")).toBe("(Recharge 5-6)");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { type IDBPDatabase, openDB } from "idb";
|
|||||||
|
|
||||||
const DB_NAME = "initiative-bestiary";
|
const DB_NAME = "initiative-bestiary";
|
||||||
const STORE_NAME = "sources";
|
const STORE_NAME = "sources";
|
||||||
const DB_VERSION = 1;
|
const DB_VERSION = 2;
|
||||||
|
|
||||||
export interface CachedSourceInfo {
|
export interface CachedSourceInfo {
|
||||||
readonly sourceCode: string;
|
readonly sourceCode: string;
|
||||||
@@ -32,12 +32,16 @@ async function getDb(): Promise<IDBPDatabase | null> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
db = await openDB(DB_NAME, DB_VERSION, {
|
db = await openDB(DB_NAME, DB_VERSION, {
|
||||||
upgrade(database) {
|
upgrade(database, oldVersion, _newVersion, transaction) {
|
||||||
if (!database.objectStoreNames.contains(STORE_NAME)) {
|
if (oldVersion < 1) {
|
||||||
database.createObjectStore(STORE_NAME, {
|
database.createObjectStore(STORE_NAME, {
|
||||||
keyPath: "sourceCode",
|
keyPath: "sourceCode",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 2 && database.objectStoreNames.contains(STORE_NAME)) {
|
||||||
|
// Clear cached creatures to pick up improved tag processing
|
||||||
|
transaction.objectStore(STORE_NAME).clear();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return db;
|
return db;
|
||||||
|
|||||||
@@ -14,6 +14,15 @@ const ATKR_MAP: Record<string, string> = {
|
|||||||
"r,m": "Melee or Ranged Attack Roll:",
|
"r,m": "Melee or Ranged Attack Roll:",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ATK_MAP: Record<string, string> = {
|
||||||
|
mw: "Melee Weapon Attack:",
|
||||||
|
rw: "Ranged Weapon Attack:",
|
||||||
|
ms: "Melee Spell Attack:",
|
||||||
|
rs: "Ranged Spell Attack:",
|
||||||
|
"mw,rw": "Melee or Ranged Weapon Attack:",
|
||||||
|
"rw,mw": "Melee or Ranged Weapon Attack:",
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strips 5etools {@tag ...} markup from text, converting to plain readable text.
|
* Strips 5etools {@tag ...} markup from text, converting to plain readable text.
|
||||||
*
|
*
|
||||||
@@ -51,11 +60,16 @@ export function stripTags(text: string): string {
|
|||||||
// {@hit N} → "+N"
|
// {@hit N} → "+N"
|
||||||
result = result.replaceAll(/\{@hit\s+(\d+)\}/g, "+$1");
|
result = result.replaceAll(/\{@hit\s+(\d+)\}/g, "+$1");
|
||||||
|
|
||||||
// {@atkr type} → mapped attack roll text
|
// {@atkr type} → mapped attack roll text (2024 rules)
|
||||||
result = result.replaceAll(/\{@atkr\s+([^}]+)\}/g, (_, type: string) => {
|
result = result.replaceAll(/\{@atkr\s+([^}]+)\}/g, (_, type: string) => {
|
||||||
return ATKR_MAP[type.trim()] ?? "Attack Roll:";
|
return ATKR_MAP[type.trim()] ?? "Attack Roll:";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// {@atk type} → mapped attack type text (pre-2024 data)
|
||||||
|
result = result.replaceAll(/\{@atk\s+([^}]+)\}/g, (_, type: string) => {
|
||||||
|
return ATK_MAP[type.trim()] ?? "Attack:";
|
||||||
|
});
|
||||||
|
|
||||||
// {@actSave ability} → "Ability saving throw"
|
// {@actSave ability} → "Ability saving throw"
|
||||||
result = result.replaceAll(
|
result = result.replaceAll(
|
||||||
/\{@actSave\s+([^}]+)\}/g,
|
/\{@actSave\s+([^}]+)\}/g,
|
||||||
|
|||||||
Reference in New Issue
Block a user