97 lines
2.5 KiB
TypeScript
97 lines
2.5 KiB
TypeScript
import type { BestiaryIndex, BestiaryIndexEntry } from "@initiative/domain";
|
|
|
|
import rawIndex from "../../../../data/bestiary/index.json";
|
|
|
|
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 = Object.fromEntries(
|
|
Object.entries(compact.sources).filter(
|
|
([code]) => !EXCLUDED_SOURCES.has(code),
|
|
),
|
|
);
|
|
cachedIndex = {
|
|
sources,
|
|
creatures: compact.creatures
|
|
.filter((c) => !EXCLUDED_SOURCES.has(c.s))
|
|
.map(mapCreature),
|
|
};
|
|
return cachedIndex;
|
|
}
|
|
|
|
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 {
|
|
const index = loadBestiaryIndex();
|
|
return index.sources[sourceCode] ?? sourceCode;
|
|
}
|