358 lines
13 KiB
TypeScript
358 lines
13 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════════════
|
|
STAT BAR — HP / MP / XP Style
|
|
═══════════════════════════════════════════════════════════════════════════ */
|
|
|
|
const statBarVariants = cva(
|
|
"relative h-6 rounded-sm border-2 overflow-hidden bg-[var(--void-900)]",
|
|
{
|
|
variants: {
|
|
variant: {
|
|
hp: "border-[var(--hp-red)]",
|
|
mp: "border-[var(--mp-blue)]",
|
|
xp: "border-[var(--xp-gold)]",
|
|
shield: "border-[var(--shield-purple)]",
|
|
},
|
|
size: {
|
|
sm: "h-4 text-[10px]",
|
|
default: "h-6 text-xs",
|
|
lg: "h-8 text-sm",
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
variant: "xp",
|
|
size: "default",
|
|
},
|
|
}
|
|
)
|
|
|
|
const statBarFillVariants = cva(
|
|
"h-full transition-all duration-500 ease-out relative",
|
|
{
|
|
variants: {
|
|
variant: {
|
|
hp: "bg-gradient-to-b from-[var(--hp-red)] to-[#990000] shadow-[inset_0_-2px_4px_rgba(0,0,0,0.3),0_0_10px_rgba(255,51,51,0.5)]",
|
|
mp: "bg-gradient-to-b from-[var(--mp-blue)] to-[#0066cc] shadow-[inset_0_-2px_4px_rgba(0,0,0,0.3),0_0_10px_rgba(51,153,255,0.5)]",
|
|
xp: "bg-gradient-to-b from-[var(--xp-gold)] to-[#cc9900] shadow-[inset_0_-2px_4px_rgba(0,0,0,0.3),0_0_10px_rgba(255,215,0,0.5)]",
|
|
shield: "bg-gradient-to-b from-[var(--shield-purple)] to-[#6a1b9a] shadow-[inset_0_-2px_4px_rgba(0,0,0,0.3),0_0_10px_rgba(156,39,176,0.5)]",
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
variant: "xp",
|
|
},
|
|
}
|
|
)
|
|
|
|
interface StatBarProps
|
|
extends React.HTMLAttributes<HTMLDivElement>,
|
|
VariantProps<typeof statBarVariants> {
|
|
value: number
|
|
max?: number
|
|
showLabel?: boolean
|
|
label?: string
|
|
animated?: boolean
|
|
}
|
|
|
|
const StatBar = React.forwardRef<HTMLDivElement, StatBarProps>(
|
|
({ className, variant, size, value, max = 100, showLabel = true, label, animated = false, ...props }, ref) => {
|
|
const percentage = Math.min(100, Math.max(0, (value / max) * 100))
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(statBarVariants({ variant, size }), className)}
|
|
{...props}
|
|
>
|
|
<div
|
|
className={cn(statBarFillVariants({ variant }), animated && "animate-[bar-fill_1s_ease-out]")}
|
|
style={{ width: `${percentage}%` }}
|
|
/>
|
|
{showLabel && (
|
|
<div className="absolute inset-0 flex items-center justify-center font-display font-bold text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.8)]">
|
|
{label || `${value}/${max}`}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
)
|
|
StatBar.displayName = "StatBar"
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════════════
|
|
LEVEL BADGE
|
|
═══════════════════════════════════════════════════════════════════════════ */
|
|
|
|
interface LevelBadgeProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
level: number
|
|
size?: "sm" | "default" | "lg"
|
|
animated?: boolean
|
|
}
|
|
|
|
const LevelBadge = React.forwardRef<HTMLDivElement, LevelBadgeProps>(
|
|
({ className, level, size = "default", animated = false, ...props }, ref) => {
|
|
const sizes = {
|
|
sm: "w-10 h-10 text-sm",
|
|
default: "w-12 h-12 text-xl",
|
|
lg: "w-16 h-16 text-2xl",
|
|
}
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
"relative inline-flex items-center justify-center rounded-full",
|
|
"bg-gradient-to-b from-[#3a3a3a] to-[#1a1a1a]",
|
|
"border-[3px] border-[var(--xp-gold)]",
|
|
"font-display font-black text-[var(--xp-gold)]",
|
|
"shadow-[0_0_20px_rgba(255,215,0,0.3),inset_0_2px_4px_rgba(255,255,255,0.1)]",
|
|
animated && "animate-[level-up_0.5s_ease-out]",
|
|
sizes[size],
|
|
className
|
|
)}
|
|
style={{ textShadow: "0 0 10px var(--xp-gold)" }}
|
|
{...props}
|
|
>
|
|
{level}
|
|
</div>
|
|
)
|
|
}
|
|
)
|
|
LevelBadge.displayName = "LevelBadge"
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════════════
|
|
ACHIEVEMENT POPUP
|
|
═══════════════════════════════════════════════════════════════════════════ */
|
|
|
|
interface AchievementProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
icon: React.ReactNode
|
|
title: string
|
|
name: string
|
|
xp?: number
|
|
animated?: boolean
|
|
}
|
|
|
|
const Achievement = React.forwardRef<HTMLDivElement, AchievementProps>(
|
|
({ className, icon, title, name, xp, animated = true, ...props }, ref) => {
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
"flex items-center gap-4 p-4",
|
|
"bg-gradient-to-b from-[rgba(30,30,40,0.95)] to-[rgba(20,20,30,0.95)]",
|
|
"border-2 border-[var(--xp-gold)] rounded-lg",
|
|
"shadow-[0_0_30px_rgba(255,215,0,0.3)]",
|
|
animated && "animate-achievement",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<div className="w-14 h-14 bg-gradient-to-br from-[var(--xp-gold)] to-[#cc9900] rounded-lg flex items-center justify-center text-2xl">
|
|
{icon}
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="font-display text-xs uppercase tracking-widest text-[var(--xp-gold)]">
|
|
{title}
|
|
</p>
|
|
<p className="font-sans text-lg font-bold text-white">{name}</p>
|
|
{xp && (
|
|
<p className="text-xs text-[var(--xp-gold)]">+{xp} XP</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
)
|
|
Achievement.displayName = "Achievement"
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════════════
|
|
SKILL TREE NODE
|
|
═══════════════════════════════════════════════════════════════════════════ */
|
|
|
|
interface SkillNodeProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
icon: React.ReactNode
|
|
name: string
|
|
unlocked?: boolean
|
|
active?: boolean
|
|
level?: number
|
|
maxLevel?: number
|
|
}
|
|
|
|
const SkillNode = React.forwardRef<HTMLDivElement, SkillNodeProps>(
|
|
({ className, icon, name, unlocked = false, active = false, level = 0, maxLevel = 5, ...props }, ref) => {
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
"flex flex-col items-center gap-2 p-3 rounded-lg transition-all duration-300",
|
|
unlocked ? "opacity-100" : "opacity-40",
|
|
active && "scale-105",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<div
|
|
className={cn(
|
|
"w-16 h-16 rounded-lg flex items-center justify-center text-2xl",
|
|
"border-2 transition-all duration-300",
|
|
unlocked
|
|
? "bg-gradient-to-b from-[var(--void-700)] to-[var(--void-900)] border-[var(--cyan-500)] glow-cyan"
|
|
: "bg-[var(--void-900)] border-[var(--void-600)]"
|
|
)}
|
|
>
|
|
{icon}
|
|
</div>
|
|
<p className="text-xs font-medium text-center">{name}</p>
|
|
{maxLevel > 1 && (
|
|
<div className="flex gap-1">
|
|
{Array.from({ length: maxLevel }).map((_, i) => (
|
|
<div
|
|
key={i}
|
|
className={cn(
|
|
"w-2 h-2 rounded-full",
|
|
i < level ? "bg-[var(--cyan-500)]" : "bg-[var(--void-600)]"
|
|
)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
)
|
|
SkillNode.displayName = "SkillNode"
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════════════
|
|
STATS PANEL
|
|
═══════════════════════════════════════════════════════════════════════════ */
|
|
|
|
interface StatsPanelProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
stats: Array<{
|
|
label: string
|
|
value: number | string
|
|
icon?: React.ReactNode
|
|
change?: number
|
|
}>
|
|
}
|
|
|
|
const StatsPanel = React.forwardRef<HTMLDivElement, StatsPanelProps>(
|
|
({ className, stats, ...props }, ref) => {
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
"p-4 rounded-lg",
|
|
"bg-gradient-to-b from-[rgba(30,30,40,0.95)] to-[rgba(20,20,30,0.95)]",
|
|
"border-2 border-[var(--xp-gold)] rounded-sm",
|
|
"shadow-[inset_0_1px_0_rgba(255,255,255,0.05),0_4px_20px_rgba(0,0,0,0.5)]",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
{/* Gaming-style header bar */}
|
|
<div className="h-1 -mt-4 -mx-4 mb-4 bg-gradient-to-r from-[var(--hp-red)] via-[var(--mp-blue)] to-[var(--xp-gold)]" />
|
|
|
|
<div className="grid gap-3">
|
|
{stats.map((stat, i) => (
|
|
<div key={i} className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
{stat.icon && <span className="text-[var(--xp-gold)]">{stat.icon}</span>}
|
|
<span className="text-xs uppercase tracking-wider text-muted-foreground">
|
|
{stat.label}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-display font-bold text-white">
|
|
{stat.value}
|
|
</span>
|
|
{stat.change !== undefined && (
|
|
<span className={cn(
|
|
"text-xs",
|
|
stat.change > 0 ? "text-[var(--success)]" : stat.change < 0 ? "text-[var(--hp-red)]" : "text-muted-foreground"
|
|
)}>
|
|
{stat.change > 0 ? "+" : ""}{stat.change}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
)
|
|
StatsPanel.displayName = "StatsPanel"
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════════════
|
|
INVENTORY SLOT
|
|
═══════════════════════════════════════════════════════════════════════════ */
|
|
|
|
interface InventorySlotProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
item?: {
|
|
icon: React.ReactNode
|
|
rarity?: "common" | "uncommon" | "rare" | "epic" | "legendary"
|
|
quantity?: number
|
|
}
|
|
size?: "sm" | "default" | "lg"
|
|
selected?: boolean
|
|
}
|
|
|
|
const rarityColors = {
|
|
common: "border-[var(--void-400)]",
|
|
uncommon: "border-[var(--success)]",
|
|
rare: "border-[var(--mp-blue)]",
|
|
epic: "border-[var(--shield-purple)]",
|
|
legendary: "border-[var(--xp-gold)] glow-gold",
|
|
}
|
|
|
|
const InventorySlot = React.forwardRef<HTMLDivElement, InventorySlotProps>(
|
|
({ className, item, size = "default", selected = false, ...props }, ref) => {
|
|
const sizes = {
|
|
sm: "w-10 h-10",
|
|
default: "w-14 h-14",
|
|
lg: "w-20 h-20",
|
|
}
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
"relative rounded-lg border-2 bg-[var(--void-900)] transition-all duration-200",
|
|
"hover:bg-[var(--void-800)] cursor-pointer",
|
|
sizes[size],
|
|
item ? rarityColors[item.rarity || "common"] : "border-[var(--void-700)]",
|
|
selected && "ring-2 ring-[var(--cyan-500)] ring-offset-2 ring-offset-background",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
{item && (
|
|
<>
|
|
<div className="absolute inset-0 flex items-center justify-center text-xl">
|
|
{item.icon}
|
|
</div>
|
|
{item.quantity && item.quantity > 1 && (
|
|
<div className="absolute bottom-0 right-0 px-1 text-[10px] font-bold bg-black/70 rounded-tl">
|
|
{item.quantity}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
)
|
|
InventorySlot.displayName = "InventorySlot"
|
|
|
|
export {
|
|
StatBar,
|
|
LevelBadge,
|
|
Achievement,
|
|
SkillNode,
|
|
StatsPanel,
|
|
InventorySlot,
|
|
}
|