"use client"; import Image from "next/image"; import Link from "next/link"; import { useRef, type PointerEvent } from "react"; import type { CatalogAscensionGroup, CatalogBaseStat, } from "@/lib/catalog/lunaris"; import { mergePosterMaterials, type PosterMaterial } from "@/lib/material-poster"; import { accentGradient } from "@/lib/overview-image"; import { cn } from "@/lib/utils"; export type PosterBlendMode = "normal" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity"; type PosterCharacter = { name: string; nameCardImageUrl: string | null; baseStats: CatalogBaseStat[]; ascension: Array & { items: PosterMaterial[] }>; leveling: Array & { items: PosterMaterial[] }>; }; const POSTER_EXPORT_WIDTH = 1600; const POSTER_IMAGE_QUALITY = 100; const MAX_OPTIMIZED_IMAGE_WIDTH = 3840; const statLabels: Record = { hp: "HP", atk: "ATK", def: "DEF", em: "EM", }; function statLabel(key: string): string { return statLabels[key.toLowerCase()] ?? key; } function statIconName(key: string): string | null { const normalized = key.toLowerCase().replace(/[^a-z]/gu, ""); if (key.includes("%") && ["hp", "atk", "def"].includes(normalized)) return `${normalized}-percent`; if (["hp", "atk", "def", "em", "er"].includes(normalized)) return normalized; if (["critrate", "crit", "criticalrate"].includes(normalized)) return "critrate"; if (["critdmg", "critdamage", "criticaldamage"].includes(normalized)) return "critdmg"; return null; } function statValue(key: string, value: number | string): string { const formatted = typeof value === "number" ? value.toLocaleString("en-US", { maximumFractionDigits: 2 }) : value; return key.includes("%") ? `${formatted}%` : formatted; } export function MaterialPoster({ character, characterArtUrl, artworkOffsetX, artworkOffsetY, artworkScale, artworkRotation, topColors, bottomColors, interactive = false, linkMaterials = false, onArtworkMove, className, bleeding = [], }: { character: PosterCharacter; characterArtUrl: string | null; artworkOffsetX: number; artworkOffsetY: number; topColors: [string, string]; bottomColors: [string, string]; interactive?: boolean; linkMaterials?: boolean; onArtworkMove?: (offset: { x: number; y: number }) => void; className?: string; artworkScale: number; artworkRotation: number; bleeding?: Array<{ id: string; imageUrl: string | null; amount: number; blendMode: PosterBlendMode }>; }) { const dragStart = useRef<{ pointerId: number; x: number; y: number; offsetX: number; offsetY: number } | null>(null); const stats = character.baseStats.slice(0, 4); const materials = mergePosterMaterials( [...character.ascension, ...character.leveling].flatMap((group) => group.items), ); const artworkImageSize = Math.min( MAX_OPTIMIZED_IMAGE_WIDTH, Math.max(POSTER_EXPORT_WIDTH, Math.ceil(POSTER_EXPORT_WIDTH * artworkScale / 100)), ); const background = `linear-gradient(180deg, ${topColors[0]} 0%, ${topColors[1]} 22%, rgba(0,0,0,0) 42%, rgba(0,0,0,0) 62%, ${bottomColors[0]} 88%, ${bottomColors[1]} 100%)`; function handlePointerDown(event: PointerEvent) { if (!interactive || !characterArtUrl || !onArtworkMove) return; event.currentTarget.setPointerCapture(event.pointerId); dragStart.current = { pointerId: event.pointerId, x: event.clientX, y: event.clientY, offsetX: artworkOffsetX, offsetY: artworkOffsetY, }; } function handlePointerMove(event: PointerEvent) { if (!dragStart.current || dragStart.current.pointerId !== event.pointerId || !onArtworkMove) return; const rect = event.currentTarget.getBoundingClientRect(); onArtworkMove({ x: dragStart.current.offsetX + ((event.clientX - dragStart.current.x) / rect.width) * 100, y: dragStart.current.offsetY + ((event.clientY - dragStart.current.y) / rect.height) * 100, }); } function handlePointerUp(event: PointerEvent) { if (dragStart.current?.pointerId === event.pointerId) dragStart.current = null; } return (
{character.nameCardImageUrl ? ( ) : null}
{characterArtUrl ? (
{character.name} {[...bleeding].reverse().map((layer) => ( ))}
) : null}

BASE STATS

{Array.from({ length: 4 }, (_, index) => { const stat = stats[index]; const iconName = stat ? statIconName(stat.key) : null; return (
{iconName ? ( ) : null} {stat ? statLabel(stat.key) : "—"} {stat ? statValue(stat.key, stat.value) : "—"}
); })}

MATERIALS

LV.90

{materials.map((material, index) => { const materialCard = ( <>
{material.imageUrl ? : null}
×{material.amount.toLocaleString("en-US")}
); const className = "aspect-[0.72] overflow-hidden rounded-[1.3cqw] bg-black/95 ring-1 ring-white/10"; return linkMaterials ? ( {materialCard} ) : (
{materialCard}
); })}
); }