108 lines
4.0 KiB
TypeScript
108 lines
4.0 KiB
TypeScript
"use client";
|
|||
|
|
|
||
|
|
import React from "react";
|
||
|
|
import { useI18n } from "@/lib/i18n";
|
||
|
|
|
||
|
|
export type MascotPose = "bow" | "punch" | "kick" | "zen";
|
||
|
|
|
||
|
|
interface MascotGuideProps {
|
||
|
|
pose?: MascotPose;
|
||
|
|
speechText?: {
|
||
|
|
en: string;
|
||
|
|
zh: string;
|
||
|
|
};
|
||
|
|
speechPosition?: "left" | "right" | "top";
|
||
|
|
className?: string;
|
||
|
|
speechClassName?: string;
|
||
|
|
size?: "sm" | "md" | "lg" | "xl" | "2xl";
|
||
|
|
animate?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
const POSE_IMAGES: Record<MascotPose, string> = {
|
||
|
|
bow: "/assets/theme/mascot_bow.png",
|
||
|
|
punch: "/assets/theme/mascot_punch.png",
|
||
|
|
kick: "/assets/theme/mascot_kick.png",
|
||
|
|
zen: "/assets/theme/mascot_zen.png",
|
||
|
|
};
|
||
|
|
|
||
|
|
export const MascotGuide: React.FC<MascotGuideProps> = ({
|
||
|
|
pose = "bow",
|
||
|
|
speechText,
|
||
|
|
speechPosition = "right",
|
||
|
|
className = "",
|
||
|
|
speechClassName = "",
|
||
|
|
size = "md",
|
||
|
|
animate = true,
|
||
|
|
}) => {
|
||
|
|
const { t, language } = useI18n();
|
||
|
|
|
||
|
|
const sizeClasses = {
|
||
|
|
sm: "w-28 h-28 sm:w-36 sm:h-36",
|
||
|
|
md: "w-40 h-40 sm:w-48 sm:h-48",
|
||
|
|
lg: "w-48 h-48 sm:w-56 sm:h-56",
|
||
|
|
xl: "w-56 h-56 sm:w-68 sm:h-68",
|
||
|
|
"2xl": "w-64 h-64 sm:w-80 sm:h-80",
|
||
|
|
}[size];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
className={`relative flex items-center select-none pointer-events-none ${
|
||
|
|
speechPosition === "left"
|
||
|
|
? "flex-row-reverse"
|
||
|
|
: speechPosition === "top"
|
||
|
|
? "flex-col-reverse items-center"
|
||
|
|
: "flex-row"
|
||
|
|
} ${className}`}
|
||
|
|
>
|
||
|
|
{/* Mascot Cutout Character */}
|
||
|
|
<div
|
||
|
|
className={`relative shrink-0 ${
|
||
|
|
animate ? "animate-cartoon-wobble" : ""
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
{/* Soft magical glow backdrop */}
|
||
|
|
<div className="absolute inset-0 rounded-full bg-amber-400/25 blur-2xl transform scale-125" />
|
||
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||
|
|
<img
|
||
|
|
src={POSE_IMAGES[pose]}
|
||
|
|
alt={`Sensei Spook - ${pose}`}
|
||
|
|
className={`${sizeClasses} object-contain filter drop-shadow-[0_12px_22px_rgba(0,0,0,0.85)] relative z-10`}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Comic Speech Bubble */}
|
||
|
|
{speechText && (
|
||
|
|
<div
|
||
|
|
className={`relative z-20 pointer-events-auto ${
|
||
|
|
speechPosition === "top"
|
||
|
|
? "mb-1"
|
||
|
|
: speechPosition === "left"
|
||
|
|
? "-mr-1 sm:mr-0"
|
||
|
|
: "-ml-3 sm:-ml-2"
|
||
|
|
} ${speechClassName}`}
|
||
|
|
>
|
||
|
|
<div className="relative px-3.5 py-2.5 rounded-2xl bg-[#1b0f2e]/95 border-2 border-amber-400/90 shadow-[0_4px_18px_rgba(0,0,0,0.85)] max-w-[240px] sm:max-w-[290px] text-left backdrop-blur-sm">
|
||
|
|
{/* Bubble arrow tail pointing towards the ghost */}
|
||
|
|
{speechPosition === "right" && (
|
||
|
|
<div className="absolute -left-2 top-1/2 -translate-y-1/2 w-0 h-0 border-y-[7px] border-y-transparent border-r-[8px] border-r-amber-400 after:content-[''] after:absolute after:-top-[6px] after:left-[2px] after:border-y-[6px] after:border-y-transparent after:border-r-[7px] after:border-r-[#1b0f2e]" />
|
||
|
|
)}
|
||
|
|
{speechPosition === "left" && (
|
||
|
|
<div className="absolute -right-2 top-1/2 -translate-y-1/2 w-0 h-0 border-y-[7px] border-y-transparent border-l-[8px] border-l-amber-400 after:content-[''] after:absolute after:-top-[6px] after:right-[2px] after:border-y-[6px] after:border-y-transparent after:border-l-[7px] after:border-l-[#1b0f2e]" />
|
||
|
|
)}
|
||
|
|
{speechPosition === "top" && (
|
||
|
|
<div className="absolute -bottom-2 left-1/2 -translate-x-1/2 w-0 h-0 border-x-[7px] border-x-transparent border-t-[8px] border-t-amber-400 after:content-[''] after:absolute after:-left-[6px] after:-top-[8px] after:border-x-[6px] after:border-x-transparent after:border-t-[7px] after:border-t-[#1b0f2e]" />
|
||
|
|
)}
|
||
|
|
|
||
|
|
<p
|
||
|
|
className={`text-amber-100 font-extrabold text-[12px] sm:text-[14px] leading-snug drop-shadow-[0_1px_2px_rgba(0,0,0,0.8)] ${
|
||
|
|
language === "en" ? "font-cinzel" : "font-dojo"
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
{t(speechText)}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|