98 lines
4.5 KiB
TypeScript
98 lines
4.5 KiB
TypeScript
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<string, BossCardDefinition>();
|
|
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<string, BossCardGroup>();
|
|
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 }] : [];
|
|
}
|