Files
buzz-sheet/components/stygian/stygian-boss-display.tsx
T
gunshiz d6153be50d
CI / Verify (push) Successful in 3m8s
CI / Build immutable images and deploy (push) Successful in 3m11s
feat : update
2026-10-05 22:40:59 +07:00

165 lines
4.7 KiB
TypeScript

import Image from "next/image";
import { CheckIcon, XIcon } from "lucide-react";
import type { ReactNode } from "react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Card, CardContent } from "@/components/ui/card";
import { LunarisText } from "@/components/public/lunaris-text";
import { parseLunarisText } from "@/lib/catalog/lunaris";
export type StygianBossDisplay = {
name: string;
imageUrl: string;
hp: number;
description: string;
mechanics: { name: string; description: string }[];
recommendedMechanics: string[];
recommendedMechanicTypes: string[];
};
function RecommendedMechanic({
mechanic,
type,
}: {
mechanic: string;
type: string;
}) {
const parts = mechanic.split(/(\{SPRITE_PRESET#\d+\})/gu);
return (
<span className="inline-flex flex-wrap items-center gap-1">
{parts.map((part, index) => {
const preset = /^\{SPRITE_PRESET#(\d+)\}$/u.exec(part)?.[1];
if (preset)
return (
<Image
key={`${part}-${index}`}
src={`https://api.lunaris.moe/data/assets/icons/${preset}.png`}
alt=""
width={20}
height={20}
className="size-5 shrink-0 object-contain"
/>
);
return part ? (
<LunarisText
key={`${part}-${index}`}
nodes={parseLunarisText(part)}
fallback={part}
/>
) : null;
})}
{type === "Disadvantage" ? (
<XIcon className="size-4 text-red-500" aria-hidden="true" />
) : type === "Advantage" ? (
<CheckIcon className="size-4 text-emerald-500" aria-hidden="true" />
) : null}
</span>
);
}
export function StygianBossTab({
boss,
position,
priority = false,
}: {
boss: StygianBossDisplay | undefined;
position: number;
priority?: boolean;
}) {
const positionLabel = ["บน", "กลาง", "ล่าง"][position];
return (
<>
{boss ? (
<Image
src={boss.imageUrl}
alt=""
width={256}
height={256}
sizes="256px"
className="size-20 shrink-0 object-contain sm:size-32 lg:size-48"
priority={priority}
/>
) : (
<span>{position + 1}</span>
)}
<span className="text-base font-semibold">{positionLabel}</span>
{boss?.recommendedMechanics.some(Boolean) ? (
<span className="mt-2 flex flex-col items-center gap-1 text-xs font-normal whitespace-normal text-muted-foreground">
{boss.recommendedMechanics.map((mechanic, index) =>
mechanic ? (
<RecommendedMechanic
key={`${mechanic}-${index}`}
mechanic={mechanic}
type={
boss.recommendedMechanicTypes[index] ??
(index === 0
? "Advantage"
: index === 1
? "Disadvantage"
: "None")
}
/>
) : null,
)}
</span>
) : null}
</>
);
}
export function StygianBossCard({
boss,
level,
children,
}: {
boss: StygianBossDisplay;
level: 105 | 110;
children: ReactNode;
}) {
return (
<Card>
<CardContent className="flex flex-col gap-6">
<header className="text-center">
<h2 className="font-heading text-lg font-semibold">{boss.name}</h2>
<p className="mt-1 text-sm text-muted-foreground">
Level {level} - HP {Math.round(boss.hp).toLocaleString("en-US")}
</p>
</header>
<div className="flex min-w-0 flex-col gap-5">
{children}
<Accordion>
<AccordionItem value="overview">
<AccordionTrigger>Boss overview</AccordionTrigger>
<AccordionContent className="whitespace-pre-line text-muted-foreground">
<LunarisText
nodes={parseLunarisText(boss.description)}
fallback={boss.description}
/>
</AccordionContent>
</AccordionItem>
{boss.mechanics.map((mechanic, index) => (
<AccordionItem
key={`${mechanic.name}-${index}`}
value={`mechanic-${index}`}
>
<AccordionTrigger>{mechanic.name}</AccordionTrigger>
<AccordionContent className="text-muted-foreground">
<LunarisText
nodes={parseLunarisText(mechanic.description)}
fallback={mechanic.description}
/>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</CardContent>
</Card>
);
}