Implement the 029-on-demand-bestiary feature that replaces the bundled XMM bestiary JSON with a compact search index (~350KB) and on-demand source loading, where users explicitly provide a URL or upload a JSON file to fetch full stat block data per source, which is then normalized and cached in IndexedDB (with in-memory fallback) so creature stat blocks load instantly on subsequent visits while keeping the app bundle small and never auto-fetching copyrighted content

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-10 22:46:13 +01:00
parent 99d1ba1bcd
commit 91120d7c82
31 changed files with 38321 additions and 63422 deletions

View File

@@ -81,9 +81,11 @@ interface RawSpellcasting {
// --- Source mapping ---
const SOURCE_DISPLAY_NAMES: Record<string, string> = {
XMM: "MM 2024",
};
let sourceDisplayNames: Record<string, string> = {};
export function setSourceDisplayNames(names: Record<string, string>): void {
sourceDisplayNames = names;
}
// --- Size mapping ---
@@ -353,7 +355,13 @@ function makeCreatureId(source: string, name: string): CreatureId {
* Normalizes raw 5etools bestiary JSON into domain Creature[].
*/
export function normalizeBestiary(raw: { monster: RawMonster[] }): Creature[] {
return raw.monster.map((m) => {
// Filter out _copy entries — these reference another source's monster
// and lack their own stats (ac, hp, cr, etc.)
const monsters = raw.monster.filter(
// biome-ignore lint/suspicious/noExplicitAny: raw JSON may have _copy field
(m) => !(m as any)._copy,
);
return monsters.map((m) => {
const crStr = extractCr(m.cr);
const ac = extractAc(m.ac);
@@ -361,7 +369,7 @@ export function normalizeBestiary(raw: { monster: RawMonster[] }): Creature[] {
id: makeCreatureId(m.source, m.name),
name: m.name,
source: m.source,
sourceDisplayName: SOURCE_DISPLAY_NAMES[m.source] ?? m.source,
sourceDisplayName: sourceDisplayNames[m.source] ?? m.source,
size: formatSize(m.size),
type: formatType(m.type),
alignment: formatAlignment(m.alignment),