Files
healer-man/src/game/bossCardCatalog.ts
T
2026-09-13 13:21:27 -04:00

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 }] : [];
}