Implement the 030-bulk-import-sources feature that adds a one-click bulk import button to load all bestiary sources at once, with real-time progress feedback in the side panel and a toast notification when the panel is closed, plus completion/failure reporting with auto-dismiss on success and persistent display on partial failure, while also hardening the bestiary normalizer to handle variable stat blocks (spell summons with special AC/HP/CR) and skip malformed monster entries gracefully

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-10 23:29:34 +01:00
parent c323adc343
commit 94d125d9c4
14 changed files with 850 additions and 106 deletions

View File

@@ -33,21 +33,61 @@ function mapCreature(c: CompactCreature): BestiaryIndexEntry {
};
}
// 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 = Object.fromEntries(
Object.entries(compact.sources).filter(
([code]) => !EXCLUDED_SOURCES.has(code),
),
);
cachedIndex = {
sources: compact.sources,
creatures: compact.creatures.map(mapCreature),
sources,
creatures: compact.creatures
.filter((c) => !EXCLUDED_SOURCES.has(c.s))
.map(mapCreature),
};
return cachedIndex;
}
export function getDefaultFetchUrl(sourceCode: string): string {
return `https://raw.githubusercontent.com/5etools-mirror-3/5etools-src/main/data/bestiary/bestiary-${sourceCode.toLowerCase()}.json`;
export function getAllSourceCodes(): string[] {
const index = loadBestiaryIndex();
return Object.keys(index.sources).filter((c) => !EXCLUDED_SOURCES.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 {