65 lines
3.1 KiB
TypeScript
65 lines
3.1 KiB
TypeScript
import { lazy, Suspense, useEffect, useRef, useState, type CSSProperties } from "react";
|
|
import { BOSS_CARD_RARITIES, BOSS_CARD_RARITY_INFO } from "../game/bossCards";
|
|
import type { DungeonBossCard as BossCard } from "../game/dungeonBossCards";
|
|
import "./dungeonBossCards.css";
|
|
|
|
const BossCardPortrait = lazy(() => import("./BossCardPortrait"));
|
|
|
|
function DungeonBossModel({ card }: { readonly card: BossCard }) {
|
|
const container = useRef<HTMLDivElement>(null);
|
|
const [visible, setVisible] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const element = container.current;
|
|
if (!element || !card.model) return;
|
|
if (typeof IntersectionObserver === "undefined") {
|
|
setVisible(true);
|
|
return;
|
|
}
|
|
const observer = new IntersectionObserver(([entry]) => setVisible(entry.isIntersecting), {
|
|
rootMargin: "40px",
|
|
threshold: 0.01,
|
|
});
|
|
observer.observe(element);
|
|
return () => observer.disconnect();
|
|
}, [card.model]);
|
|
|
|
return <div ref={container} className="dungeon-boss-card__portrait">
|
|
{!card.model ? <span className="dungeon-boss-card__model-status">Model unavailable</span>
|
|
: visible ? <Suspense fallback={<span className="dungeon-boss-card__model-status">Loading model…</span>}>
|
|
<BossCardPortrait model={card.model} name={card.name} installed />
|
|
</Suspense> : <span className="dungeon-boss-card__model-status">Loading model…</span>}
|
|
</div>;
|
|
}
|
|
|
|
export function DungeonBossCards({ cards, compact = false }: {
|
|
readonly cards: readonly BossCard[];
|
|
readonly compact?: boolean;
|
|
}) {
|
|
if (!cards.length) return <p className="dungeon-boss-cards__empty">No boss cards are listed for this dungeon.</p>;
|
|
return <div className={`dungeon-boss-cards ${compact ? "dungeon-boss-cards--compact" : ""}`}>
|
|
{cards.map((card, index) => {
|
|
const highestRarity = card.ownedRarities.at(-1);
|
|
const accent = highestRarity ? BOSS_CARD_RARITY_INFO[highestRarity].color : "#9a947f";
|
|
return <article key={card.key} className={`dungeon-boss-card ${card.copies ? "is-owned" : ""}`}
|
|
style={{ "--boss-card-accent": accent } as CSSProperties}
|
|
aria-label={`${card.name}, ${card.copies ? `${card.copies} ${card.copies === 1 ? "card" : "cards"} owned` : "not collected"}`}>
|
|
<header><span>Boss {String(index + 1).padStart(2, "0")}</span><strong>{card.copies ? "Collected" : "Uncollected"}</strong></header>
|
|
<DungeonBossModel card={card} />
|
|
<div className="dungeon-boss-card__body">
|
|
<h3>{card.name}</h3>
|
|
<p>{card.copies ? `${card.copies} ${card.copies === 1 ? "copy" : "copies"} owned` : "Card not collected"}</p>
|
|
</div>
|
|
<footer aria-label="Card rarities">
|
|
{BOSS_CARD_RARITIES.map((rarity) => <span key={rarity}
|
|
className={card.ownedRarities.includes(rarity) ? "is-owned" : ""}
|
|
style={{ "--rarity-color": BOSS_CARD_RARITY_INFO[rarity].color } as CSSProperties}
|
|
title={`${BOSS_CARD_RARITY_INFO[rarity].label}: ${card.ownedRarities.includes(rarity) ? "owned" : "not owned"}`}>
|
|
{rarity.charAt(0).toUpperCase()}
|
|
</span>)}
|
|
</footer>
|
|
</article>;
|
|
})}
|
|
</div>;
|
|
}
|