168 lines
5.0 KiB
TypeScript
168 lines
5.0 KiB
TypeScript
import Image from "@/components/ui/resilient-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";
|
|
import { stygianBossPositionLabels } from "@/lib/stygian/constants";
|
|
|
|
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 = stygianBossPositionLabels[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,
|
|
mechanicsId,
|
|
}: {
|
|
boss: StygianBossDisplay;
|
|
level: 105 | 110;
|
|
children: ReactNode;
|
|
mechanicsId?: string;
|
|
}) {
|
|
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} - HP {Math.round(boss.hp).toLocaleString("en-US")}
|
|
</p>
|
|
</header>
|
|
<div className="flex min-w-0 flex-col gap-5">
|
|
{children}
|
|
<Accordion id={mechanicsId} defaultValue={mechanicsId ? ["overview"] : undefined} className="scroll-mt-6">
|
|
<AccordionItem value="overview">
|
|
<AccordionTrigger>ข้อมูลและกลไกของบอส</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>
|
|
);
|
|
}
|