import type { ClassId } from "../app/characterCatalog"; export interface TalentUiTree { readonly id: string; readonly classId: ClassId; readonly name: string; readonly icon: string; readonly background: string; readonly description: string; readonly system?: "classic" | "coa"; } export interface TalentUiNode { readonly id: string; readonly treeId: string; readonly index: number; readonly prerequisiteId: string | null; readonly prerequisiteRank: number | null; readonly prerequisites: readonly { readonly talentId: string; readonly requiredRank: number; }[]; readonly column: number; readonly row: number; readonly icon: string; readonly maxRank: number; readonly name: string; readonly description: string; readonly rankDescriptions: readonly string[]; readonly system?: "classic" | "coa"; readonly entryType?: "Ability" | "Talent"; readonly abilityEssenceCost?: number; readonly talentEssenceCost?: number; readonly requiredClassPoints?: number; readonly requiredSpecPoints?: number; readonly requiredLevel?: number; readonly isPassive?: boolean; } export interface TalentUiCatalog { readonly schemaVersion: 1; readonly classId: ClassId; readonly trees: readonly TalentUiTree[]; readonly nodes: readonly TalentUiNode[]; } interface TalentUiCatalogModule { readonly default: TalentUiCatalog; } const shardLoaders = import.meta.glob("./generated/talentUi/*.json"); const loadCache = new Map>(); const loadedCatalogs = new Map(); export function loadTalentUiCatalog(classId: ClassId): Promise { const cached = loadCache.get(classId); if (cached) return cached; const key = `./generated/talentUi/${classId}.json`; const loader = shardLoaders[key]; if (!loader) return Promise.reject(new Error(`No talent UI catalog exists for ${classId}.`)); const pending = loader().then((module) => { if (module.default.schemaVersion !== 1 || module.default.classId !== classId) { throw new Error(`Talent UI catalog ${classId} has incompatible metadata.`); } loadedCatalogs.set(classId, module.default); return module.default; }).catch((error: unknown) => { loadCache.delete(classId); throw error; }); loadCache.set(classId, pending); return pending; } export function loadedTalentUiCatalog(classId: ClassId): TalentUiCatalog | null { return loadedCatalogs.get(classId) ?? null; } export function talentUiDescriptionForRank(node: TalentUiNode, rank: number): string { const normalizedRank = Math.min( node.maxRank, Math.max(1, Math.trunc(Number.isFinite(rank) ? rank : 1)), ); return node.rankDescriptions[normalizedRank - 1] ?? node.description; } export function talentUiTreeById(catalog: TalentUiCatalog, treeId: string): TalentUiTree | null { return catalog.trees.find((tree) => tree.id === treeId) ?? null; } export function talentUiNodeById(catalog: TalentUiCatalog, nodeId: string): TalentUiNode | null { return catalog.nodes.find((node) => node.id === nodeId) ?? null; } export function talentUiNodesForTree(catalog: TalentUiCatalog, treeId: string): readonly TalentUiNode[] { return catalog.nodes .filter((node) => node.treeId === treeId) .sort((left, right) => left.row - right.row || left.index - right.index); } export function talentUiArtUrls(catalog: TalentUiCatalog): readonly string[] { return [...new Set([ ...catalog.trees.flatMap((tree) => [tree.icon, tree.background]), ...catalog.nodes.map((node) => node.icon), ])]; }