import { DUNGEON_DEFINITIONS } from "./dungeonRegistry"; import type { DungeonDefinition } from "./dungeonTypes"; import type { MobModelStyle } from "./mobPopulation"; import { BOSS_CARD_RARITIES, type BossCardCollection, type BossCardRarity } from "./bossCards"; export interface BossCardDefinition { readonly key: string; readonly name: string; readonly category: "wow" | "rom"; readonly model?: MobModelStyle; readonly dungeons: readonly { readonly id: string; readonly title: string }[]; readonly sources: readonly { readonly dungeonId: string; readonly entityId: string }[]; } /** Same boss in multiple dungeons shares a collection, while WoW and RoM remain distinct. */ export function buildBossCardCatalog(definitions: readonly DungeonDefinition[]): readonly BossCardDefinition[] { const bosses = new Map(); for (const dungeon of definitions) { for (const entity of Object.values(dungeon.entities)) { if (entity.kind !== "boss") continue; const category = dungeon.expansion === "runewaker" ? "rom" : "wow"; const key = `${category}:${entity.name.trim().toLowerCase().replace(/\s+/g, " ")}`; const existing = bosses.get(key); bosses.set(key, { key, name: existing?.name ?? entity.name, category, model: existing?.model ?? entity.visual.model, dungeons: existing?.dungeons.some((entry) => entry.id === dungeon.id) ? existing.dungeons : [...(existing?.dungeons ?? []), { id: dungeon.id, title: dungeon.title }], sources: [...(existing?.sources ?? []), { dungeonId: dungeon.id, entityId: entity.id }], }); } } return [...bosses.values()].map((boss) => ({ ...boss, dungeons: [...boss.dungeons].sort((a, b) => a.title.localeCompare(b.title)), })).sort((a, b) => a.name.localeCompare(b.name) || a.key.localeCompare(b.key)); } export const BOSS_CARD_CATALOG = buildBossCardCatalog(DUNGEON_DEFINITIONS); const cardsByKey = new Map(BOSS_CARD_CATALOG.map((card) => [card.key, card])); export function bossCardByKey(key: string): BossCardDefinition | null { return cardsByKey.get(key) ?? null; } export function bossCardForSource(sourceId: string, dungeonId: string, name?: string): BossCardDefinition | null { return BOSS_CARD_CATALOG.find((card) => card.sources.some( (source) => source.dungeonId === dungeonId && source.entityId === sourceId, )) ?? BOSS_CARD_CATALOG.find((card) => card.sources.some((source) => source.entityId === sourceId)) ?? BOSS_CARD_CATALOG.find((card) => card.name === name && card.dungeons.some((entry) => entry.id === dungeonId)) ?? null; } export interface BossCardRow { readonly boss: BossCardDefinition; readonly rarity: BossCardRarity; readonly copies: number; } export type BossCardGrouping = "all" | "dungeon" | "rarity"; export interface BossCardGroup { readonly key: string; readonly title: string; readonly rows: readonly BossCardRow[]; } export function groupBossCards( collection: BossCardCollection, options: { readonly earnedOnly: boolean; readonly grouping: BossCardGrouping; readonly search?: string; readonly category?: "all" | "wow" | "rom" }, catalog = BOSS_CARD_CATALOG, ): readonly BossCardGroup[] { const query = options.search?.trim().toLowerCase() ?? ""; const rows = catalog.flatMap((boss) => { if (options.category && options.category !== "all" && boss.category !== options.category) return []; if (query && !`${boss.name} ${boss.dungeons.map((entry) => entry.title).join(" ")}`.toLowerCase().includes(query)) return []; return BOSS_CARD_RARITIES.flatMap((rarity) => { const copies = collection.counts[boss.key]?.[rarity] ?? 0; return options.earnedOnly && !copies ? [] : [{ boss, rarity, copies }]; }); }); if (options.grouping === "rarity") { return BOSS_CARD_RARITIES.map((rarity) => ({ key: rarity, title: `${rarity.charAt(0).toUpperCase()}${rarity.slice(1)} cards`, rows: rows.filter((row) => row.rarity === rarity), })).filter((group) => group.rows.length); } if (options.grouping === "dungeon") { const groups = new Map(); for (const row of rows) for (const dungeon of row.boss.dungeons) { const existing = groups.get(dungeon.id); groups.set(dungeon.id, { key: dungeon.id, title: dungeon.title, rows: [...(existing?.rows ?? []), row] }); } return [...groups.values()].sort((a, b) => a.title.localeCompare(b.title)); } return rows.length ? [{ key: "all", title: "All boss cards", rows }] : []; }