import { lazy, Suspense, useMemo, useState, type CSSProperties } from "react"; import { useShellStore } from "../app/shellStore"; import { useGameStore } from "../game/store"; import { BOSS_CARD_CATALOG, groupBossCards, type BossCardGrouping } from "../game/bossCardCatalog"; import { BOSS_CARD_RARITIES, BOSS_CARD_RARITY_INFO, type BossCardCollection as Collection } from "../game/bossCards"; import { useMenuController, type MenuAction } from "../input/useMenuController"; import { ControllerButton } from "./ControllerButton"; import { dungeonContentIsUsable } from "../content/contentManager"; import { useContentManagerSnapshot } from "../content/useContentManager"; import "./bossCards.css"; const BossCardPortrait = lazy(() => import("./BossCardPortrait")); const PAGE_SIZE = 8; export function BossCardCollection({ onClose, collection }: { readonly onClose: () => void; readonly collection: Collection; }) { useContentManagerSnapshot(); const [earnedOnly, setEarnedOnly] = useState(false); const [grouping, setGrouping] = useState("all"); const [category, setCategory] = useState<"all" | "wow" | "rom">("all"); const [search, setSearch] = useState(""); const [page, setPage] = useState(0); const groups = useMemo(() => groupBossCards(collection, { earnedOnly, grouping, search, category }), [collection, earnedOnly, grouping, search, category]); const rows = useMemo(() => groups.flatMap((group) => group.rows.map((row) => ({ ...row, group }))), [groups]); const pageCount = Math.max(1, Math.ceil(rows.length / PAGE_SIZE)); const currentPage = Math.min(page, pageCount - 1); const visibleRows = rows.slice(currentPage * PAGE_SIZE, (currentPage + 1) * PAGE_SIZE); const summary = useMemo(() => { const counts = BOSS_CARD_CATALOG.flatMap((boss) => BOSS_CARD_RARITIES.map((rarity) => collection.counts[boss.key]?.[rarity] ?? 0)); return { earned: counts.filter((copies) => copies > 0).length, copies: counts.reduce((sum, copies) => sum + copies, 0), total: counts.length }; }, [collection]); const filter = (earned: boolean) => { setEarnedOnly(earned); setPage(0); }; const groupBy = (value: BossCardGrouping) => { setGrouping(value); setPage(0); }; const actions: MenuAction[] = [ { id: "cards-close", run: onClose }, { id: "cards-all", run: () => filter(false) }, { id: "cards-earned", run: () => filter(true) }, { id: "cards-boss", run: () => groupBy("all") }, { id: "cards-dungeon", run: () => groupBy("dungeon") }, { id: "cards-rarity", run: () => groupBy("rarity") }, { id: "cards-prev", enabled: currentPage > 0, run: () => setPage(currentPage - 1) }, { id: "cards-next", enabled: currentPage + 1 < pageCount, run: () => setPage(currentPage + 1) }, ]; const controller = useMenuController(actions, { initialId: "cards-all", onBack: onClose }); const controlProps = { selectedId: controller.selectedId, select: controller.select }; return
event.stopPropagation()}>

Account-wide collection log

Boss Cards

×
{summary.earned} / {summary.total} cards earned{summary.copies.toLocaleString()} total copies

Every boss awards Normal. Rare, Epic, and Legendary roll independently as bonus cards.

filter(false)}>All filter(true)}>Earned
groupBy("all")}>By boss groupBy("dungeon")}>Group by dungeon groupBy("rarity")}>Group by rarity
setPage(currentPage - 1)}>Previous Page {currentPage + 1} / {pageCount} · {rows.length} card{rows.length === 1 ? "" : "s"}{grouping === "dungeon" ? " across dungeons" : ""} = pageCount} onClick={() => setPage(currentPage + 1)}>Next
{visibleRows.length ?
{visibleRows.map((row, index) => { const rarity = BOSS_CARD_RARITY_INFO[row.rarity]; return
{grouping !== "all" && (index === 0 || visibleRows[index - 1].group.key !== row.group.key) &&

{row.group.title}

}
{rarity.label}{rarity.odds}
Loading boss model…}> dungeonContentIsUsable(entry.id))} />

{row.boss.name}

{row.boss.category === "wow" ? "World of Warcraft" : "Runes of Magic"}

{row.boss.dungeons.map((entry) => entry.title).join(" · ")}

{row.copies.toLocaleString()} {row.copies === 1 ? "copy" : "copies"}{row.copies ? "Earned" : "Not earned"}
; })}
:

{earnedOnly ? "No earned cards match these filters. Defeat a boss to begin your collection." : "No boss cards match your search."}

}
; } export default function BossCardCollectionScreen({ onClose }: { readonly onClose?: () => void }) { const collection = useShellStore((state) => state.bossCards); return useGameStore.getState().closeOverlay())} collection={collection} />; }