D&D creatures listed in data/bestiary/dnd-bundled.json are now merged into the search index and pre-loaded into creatureMap, so they appear alongside 5etools creatures with no "Load source" step. Source codes are derived from the JSON itself (each creature carries source + sourceDisplayName), so adding a new book is a pure data change. Bundled sources are excluded from getAllSourceCodes() so bulk-import skips them, and they never appear in the source manager (which only lists cached sources). Includes a reference extractor (scripts/extract-great-labors.py) for the 5.5e revised stat-block format and a /bundle-bestiary skill that future agents can follow to add monsters from other PDF books. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
110 lines
2.8 KiB
TypeScript
110 lines
2.8 KiB
TypeScript
import type { BestiaryIndex, BestiaryIndexEntry } from "@initiative/domain";
|
|
|
|
import rawIndex from "../../../../data/bestiary/index.json";
|
|
import {
|
|
getBundledDndSources,
|
|
loadBundledDndIndexEntries,
|
|
} from "./dnd-bundled-adapter.js";
|
|
|
|
interface CompactCreature {
|
|
readonly n: string;
|
|
readonly s: string;
|
|
readonly ac: number;
|
|
readonly hp: number;
|
|
readonly dx: number;
|
|
readonly cr: string;
|
|
readonly ip: number;
|
|
readonly sz: string;
|
|
readonly tp: string;
|
|
}
|
|
|
|
interface CompactIndex {
|
|
readonly sources: Record<string, string>;
|
|
readonly creatures: readonly CompactCreature[];
|
|
}
|
|
|
|
function mapCreature(c: CompactCreature): BestiaryIndexEntry {
|
|
return {
|
|
name: c.n,
|
|
source: c.s,
|
|
ac: c.ac,
|
|
hp: c.hp,
|
|
dex: c.dx,
|
|
cr: c.cr,
|
|
initiativeProficiency: c.ip,
|
|
size: c.sz,
|
|
type: c.tp,
|
|
};
|
|
}
|
|
|
|
// Source codes whose filename on the remote differs from a simple lowercase.
|
|
// Plane Shift sources use a hyphen: PSA -> ps-a, etc.
|
|
const FILENAME_OVERRIDES: Record<string, string> = {
|
|
PSA: "ps-a",
|
|
PSD: "ps-d",
|
|
PSI: "ps-i",
|
|
PSK: "ps-k",
|
|
PSX: "ps-x",
|
|
PSZ: "ps-z",
|
|
};
|
|
|
|
// Source codes with no corresponding remote bestiary file.
|
|
// Excluded from the index entirely so creatures aren't searchable
|
|
// without a fetchable source.
|
|
const EXCLUDED_SOURCES = new Set<string>([]);
|
|
|
|
let cachedIndex: BestiaryIndex | undefined;
|
|
|
|
export function loadBestiaryIndex(): BestiaryIndex {
|
|
if (cachedIndex) return cachedIndex;
|
|
|
|
const compact = rawIndex as unknown as CompactIndex;
|
|
const sources: Record<string, string> = Object.fromEntries(
|
|
Object.entries(compact.sources).filter(
|
|
([code]) => !EXCLUDED_SOURCES.has(code),
|
|
),
|
|
);
|
|
for (const [code, name] of getBundledDndSources()) {
|
|
sources[code] = name;
|
|
}
|
|
cachedIndex = {
|
|
sources,
|
|
creatures: [
|
|
...compact.creatures
|
|
.filter((c) => !EXCLUDED_SOURCES.has(c.s))
|
|
.map(mapCreature),
|
|
...loadBundledDndIndexEntries(),
|
|
],
|
|
};
|
|
return cachedIndex;
|
|
}
|
|
|
|
export function getAllSourceCodes(): string[] {
|
|
const index = loadBestiaryIndex();
|
|
const bundled = getBundledDndSources();
|
|
return Object.keys(index.sources).filter(
|
|
(c) => !EXCLUDED_SOURCES.has(c) && !bundled.has(c),
|
|
);
|
|
}
|
|
|
|
function sourceCodeToFilename(sourceCode: string): string {
|
|
return FILENAME_OVERRIDES[sourceCode] ?? sourceCode.toLowerCase();
|
|
}
|
|
|
|
export function getDefaultFetchUrl(
|
|
sourceCode: string,
|
|
baseUrl?: string,
|
|
): string {
|
|
const filename = `bestiary-${sourceCodeToFilename(sourceCode)}.json`;
|
|
if (baseUrl !== undefined) {
|
|
const normalized = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
return `${normalized}${filename}`;
|
|
}
|
|
return `https://raw.githubusercontent.com/5etools-mirror-3/5etools-src/main/data/bestiary/${filename}`;
|
|
}
|
|
|
|
export function getSourceDisplayName(sourceCode: string): string {
|
|
const index = loadBestiaryIndex();
|
|
return index.sources[sourceCode] ?? sourceCode;
|
|
}
|