diff --git a/app/blocks/gaming/gaming.tsx b/app/blocks/gaming/gaming.tsx index 8b56c168..3b5058b4 100644 --- a/app/blocks/gaming/gaming.tsx +++ b/app/blocks/gaming/gaming.tsx @@ -7,6 +7,7 @@ import PauseMenu from "@/components/ui/8bit/blocks/pause-menu"; import EnemyHealthDisplay from "@/components/ui/8bit/enemy-health-display"; import HealthBar from "@/components/ui/8bit/health-bar"; import ManaBar from "@/components/ui/8bit/mana-bar"; +import StatusEffectIndicator from "@/components/ui/8bit/status-effect-indicator"; import AudioSettings from "../../../components/ui/8bit/blocks/audio-settings"; import Leaderboard from "../../../components/ui/8bit/leaderboard"; @@ -356,6 +357,105 @@ export default function GamingBlocks() { + +
+
+

Status Effects

+
+ + +
+
+
+
+
+

Player Status Effects

+ +
+ +
+

Boss Debuffs

+ +
+ +
+

All Effect Types

+ +
+
+
+
); } diff --git a/app/blocks/status-effects/page.tsx b/app/blocks/status-effects/page.tsx new file mode 100644 index 00000000..46608159 --- /dev/null +++ b/app/blocks/status-effects/page.tsx @@ -0,0 +1,26 @@ +import CopyCommandButton from "@/app/docs/components/copy-command-button"; +import { OpenInV0Button } from "@/app/docs/components/open-in-v0-button"; + +import StatusEffectsBlock from "./status-effects"; + +export default function StatusEffectsPage() { + return ( +
+
+
+

+ Status Effect Indicator Component +

+
+ + +
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/app/blocks/status-effects/status-effects.tsx b/app/blocks/status-effects/status-effects.tsx new file mode 100644 index 00000000..da01c57e --- /dev/null +++ b/app/blocks/status-effects/status-effects.tsx @@ -0,0 +1,346 @@ +"use client"; +import { useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + StatusEffectIndicator, + StatusEffect, +} from "@/components/ui/8bit/status-effect-indicator"; + +// Sample status effects data +const sampleEffects: StatusEffect[] = [ + { + id: "poison-1", + type: "poison", + duration: 10, + stacks: 3, + intensity: "normal", + }, + { + id: "freeze-1", + type: "freeze", + duration: 5, + intensity: "strong", + }, + { + id: "burn-1", + type: "burn", + duration: 8, + stacks: 2, + intensity: "normal", + }, + { + id: "stun-1", + type: "stun", + duration: 3, + intensity: "weak", + }, + { + id: "heal-1", + type: "heal", + duration: 15, + intensity: "strong", + }, + { + id: "shield-1", + type: "shield", + duration: 20, + stacks: 1, + intensity: "normal", + }, + { + id: "speed-1", + type: "speed", + duration: 12, + intensity: "strong", + }, + { + id: "slow-1", + type: "slow", + duration: 6, + stacks: 2, + intensity: "normal", + }, +]; + +const debuffEffects = sampleEffects.filter(effect => + ['poison', 'freeze', 'burn', 'stun', 'slow'].includes(effect.type) +); + +const buffEffects = sampleEffects.filter(effect => + ['heal', 'shield', 'speed'].includes(effect.type) +); + +export default function StatusEffectsBlock() { + const [showDuration, setShowDuration] = useState(true); + const [showStacks, setShowStacks] = useState(true); + const [animated, setAnimated] = useState(true); + const [size, setSize] = useState<"sm" | "md" | "lg" | "xl">("md"); + const [maxEffects, setMaxEffects] = useState(10); + + const toggleShowDuration = () => setShowDuration(!showDuration); + const toggleShowStacks = () => setShowStacks(!showStacks); + const toggleAnimated = () => setAnimated(!animated); + + const cycleSizes = () => { + const sizes: Array<"sm" | "md" | "lg" | "xl"> = ["sm", "md", "lg", "xl"]; + const currentIndex = sizes.indexOf(size); + const nextIndex = (currentIndex + 1) % sizes.length; + setSize(sizes[nextIndex]); + }; + + const toggleMaxEffects = () => { + setMaxEffects(maxEffects === 10 ? 3 : 10); + }; + + return ( +
+ {/* Controls */} + + + Status Effect Controls + + +
+ + + + + +
+
+
+ + {/* Basic Examples */} +
+

Basic Examples

+
+ + + All Effects + + + + + + + + + Debuffs Only + + + + + +
+
+ + {/* Size Examples */} +
+

Size Examples

+
+ + + Small (SM) + + + + + + + + + Medium (MD) + + + + + + + + + Large (LG) + + + + + + + + + Extra Large (XL) + + + + + +
+
+ + {/* Individual Effect Types */} +
+

Individual Effect Types

+
+ {Object.entries({ + Poison: [{ id: "poison-demo", type: "poison" as const, duration: 10, stacks: 3, intensity: "normal" as const }], + Freeze: [{ id: "freeze-demo", type: "freeze" as const, duration: 5, intensity: "strong" as const }], + Burn: [{ id: "burn-demo", type: "burn" as const, duration: 8, stacks: 2, intensity: "normal" as const }], + Stun: [{ id: "stun-demo", type: "stun" as const, duration: 3, intensity: "weak" as const }], + Heal: [{ id: "heal-demo", type: "heal" as const, duration: 15, intensity: "strong" as const }], + Shield: [{ id: "shield-demo", type: "shield" as const, duration: 20, stacks: 1, intensity: "normal" as const }], + Speed: [{ id: "speed-demo", type: "speed" as const, duration: 12, intensity: "strong" as const }], + Slow: [{ id: "slow-demo", type: "slow" as const, duration: 6, stacks: 2, intensity: "normal" as const }], + }).map(([name, effects]) => ( + + + {name} + + + + + + ))} +
+
+ + {/* Intensity Examples */} +
+

Effect Intensities

+
+ + + Weak Intensity + + + + + + + + + Normal Intensity + + + + + + + + + Strong Intensity + + + + + +
+
+ + {/* Empty State */} +
+

Empty State

+ + + No Effects + + + +

+ When no effects are present, the component renders nothing. +

+
+
+
+
+ ); +} \ No newline at end of file diff --git a/components/ui/8bit/status-effect-indicator.tsx b/components/ui/8bit/status-effect-indicator.tsx new file mode 100644 index 00000000..00ab9500 --- /dev/null +++ b/components/ui/8bit/status-effect-indicator.tsx @@ -0,0 +1,828 @@ +"use client"; + +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +import "./styles/retro.css"; + +export interface StatusEffect { + id: string; + type: + | "poison" + | "freeze" + | "burn" + | "stun" + | "heal" + | "shield" + | "speed" + | "slow"; + duration?: number; + stacks?: number; + intensity?: "weak" | "normal" | "strong"; +} + +export const statusEffectVariants = cva("", { + variants: { + variant: { + default: "", + retro: "retro", + }, + size: { + sm: "w-16 h-16", + md: "w-20 h-20", + lg: "w-24 h-24", + xl: "w-28 h-28", + }, + }, + defaultVariants: { + variant: "retro", + size: "md", + }, +}); + +export interface StatusEffectIndicatorProps + extends React.ComponentProps<"div">, + VariantProps { + effects: StatusEffect[]; + showDuration?: boolean; + showStacks?: boolean; + maxEffects?: number; + className?: string; + animated?: boolean; +} + +// Pixel art SVG icons for different status effects +const getStatusEffectIcon = ( + type: StatusEffect["type"], + intensity: StatusEffect["intensity"] = "normal", +) => { + const icons = { + poison: ( + + {/* Bottle neck */} + + + + + {/* Bottle stopper */} + + + + + {/* Bottle body */} + + + + + + + + + + + + + + + + {/* Bottle bottom */} + + + + + + + {/* Liquid inside */} + + + + + + + + + + + + + + ), + freeze: ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + burn: ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + stun: ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + heal: ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + shield: ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + speed: ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + slow: ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + }; + + // Return the same icon for all intensities for now, can be customized later + return icons[type]; +}; + +// Get status effect colors - strong backgrounds with high contrast white icons +const getStatusEffectColor = ( + type: StatusEffect["type"], + intensity: StatusEffect["intensity"] = "normal", +) => { + const colors = { + poison: { + weak: "bg-purple-600 border-purple-400 text-white", + normal: "bg-purple-700 border-purple-500 text-white", + strong: "bg-purple-800 border-purple-600 text-white", + }, + freeze: { + weak: "bg-blue-600 border-blue-400 text-white", + normal: "bg-blue-700 border-blue-500 text-white", + strong: "bg-blue-800 border-blue-600 text-white", + }, + burn: { + weak: "bg-red-600 border-red-400 text-white", + normal: "bg-red-700 border-red-500 text-white", + strong: "bg-red-800 border-red-600 text-white", + }, + stun: { + weak: "bg-yellow-600 border-yellow-400 text-white", + normal: "bg-yellow-700 border-yellow-500 text-white", + strong: "bg-yellow-800 border-yellow-600 text-white", + }, + heal: { + weak: "bg-green-600 border-green-400 text-white", + normal: "bg-green-700 border-green-500 text-white", + strong: "bg-green-800 border-green-600 text-white", + }, + shield: { + weak: "bg-slate-600 border-slate-400 text-white", + normal: "bg-slate-700 border-slate-500 text-white", + strong: "bg-slate-800 border-slate-600 text-white", + }, + speed: { + weak: "bg-cyan-600 border-cyan-400 text-white", + normal: "bg-cyan-700 border-cyan-500 text-white", + strong: "bg-cyan-800 border-cyan-600 text-white", + }, + slow: { + weak: "bg-orange-600 border-orange-400 text-white", + normal: "bg-orange-700 border-orange-500 text-white", + strong: "bg-orange-800 border-orange-600 text-white", + }, + }; + + return colors[type][intensity]; +}; + +function StatusEffectBadge({ + effect, + size, + showDuration, + showStacks, + animated, +}: { + effect: StatusEffect; + size: VariantProps["size"]; + showDuration?: boolean; + showStacks?: boolean; + animated?: boolean; +}) { + const icon = getStatusEffectIcon(effect.type, effect.intensity); + const colorClass = getStatusEffectColor(effect.type, effect.intensity); + + // Get size-specific dimensions for indicators + const indicatorSize = + size === "sm" + ? "w-5 h-5" + : size === "md" + ? "w-6 h-6" + : size === "lg" + ? "w-7 h-7" + : "w-8 h-8"; + const textSize = + size === "sm" + ? "text-[10px]" + : size === "md" + ? "text-xs" + : size === "lg" + ? "text-sm" + : "text-base"; + + return ( +
+ {/* Container with 8-bit border styling - matches card/input pattern exactly */} +
+ {/* Main effect icon */} +
+
{icon}
+
+ + {/* Left and right borders */} + + + {/* Duration indicator - top right corner, clean style */} + {showDuration && effect.duration && ( +
+ {effect.duration} +
+ )} + + {/* Stack indicator - bottom right corner, clean style */} + {showStacks && effect.stacks && effect.stacks > 1 && ( +
+ {effect.stacks} +
+ )} +
+ ); +} + +export function StatusEffectIndicator({ + effects, + showDuration = true, + showStacks = true, + maxEffects = 10, + className, + variant, + size = "md", + animated = true, + ...props +}: StatusEffectIndicatorProps) { + const visibleEffects = React.useMemo(() => { + return effects.slice(0, maxEffects); + }, [effects, maxEffects]); + + const remainingCount = effects.length - maxEffects; + + if (effects.length === 0) { + return null; + } + + return ( +
+ {visibleEffects.map((effect) => ( + + ))} + + {/* Show remaining count if there are more effects - matches new 8-bit style */} + {remainingCount > 0 && ( +
+
+
1 ? "s" : ""}`} + > + + +{remainingCount} + +
+ +
+ )} +
+ ); +} + +export default StatusEffectIndicator; diff --git a/public/r/registry.json b/public/r/registry.json index 0a9138c7..aaeacd43 100644 --- a/public/r/registry.json +++ b/public/r/registry.json @@ -1487,6 +1487,25 @@ "target": "components/ui/8bit/separator.tsx" } ] + }, + { + "name": "status-effect-indicator", + "type": "registry:component", + "title": "8-bit Status Effect Indicator", + "description": "A retro-styled status effect indicator component for displaying pixel art icons of various game status effects.", + "registryDependencies": [], + "files": [ + { + "path": "components/ui/8bit/status-effect-indicator.tsx", + "type": "registry:component", + "target": "components/ui/8bit/status-effect-indicator.tsx" + }, + { + "path": "components/ui/8bit/styles/retro.css", + "type": "registry:component", + "target": "components/ui/8bit/styles/retro.css" + } + ] } ] } diff --git a/public/r/status-effect-indicator.json b/public/r/status-effect-indicator.json new file mode 100644 index 00000000..051e02e5 --- /dev/null +++ b/public/r/status-effect-indicator.json @@ -0,0 +1,21 @@ +{ + "name": "status-effect-indicator", + "type": "registry:component", + "title": "8-bit Status Effect Indicator", + "description": "A retro-styled status effect indicator component for displaying pixel art icons of various game status effects.", + "registryDependencies": [], + "files": [ + { + "path": "components/ui/8bit/status-effect-indicator.tsx", + "content": "\"use client\";\nimport * as React from \"react\";\n\nimport { type VariantProps, cva } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nimport \"./styles/retro.css\";\n\nexport interface StatusEffect {\n id: string;\n type: \"poison\" | \"freeze\" | \"burn\" | \"stun\" | \"heal\" | \"shield\" | \"speed\" | \"slow\";\n duration?: number;\n stacks?: number;\n intensity?: \"weak\" | \"normal\" | \"strong\";\n}\n\nexport const statusEffectVariants = cva(\"\", {\n variants: {\n variant: {\n default: \"\",\n retro: \"retro\",\n },\n size: {\n sm: \"w-8 h-8 text-sm\",\n md: \"w-10 h-10 text-base\", \n lg: \"w-12 h-12 text-lg\",\n xl: \"w-14 h-14 text-xl\",\n },\n },\n defaultVariants: {\n variant: \"retro\",\n size: \"md\",\n },\n});\n\nexport interface StatusEffectIndicatorProps\n extends React.ComponentProps<\"div\">,\n VariantProps {\n effects: StatusEffect[];\n showDuration?: boolean;\n showStacks?: boolean;\n maxEffects?: number;\n className?: string;\n animated?: boolean;\n}\n\n// Pixel art icons for different status effects\nconst getStatusEffectIcon = (\n type: StatusEffect[\"type\"],\n intensity: StatusEffect[\"intensity\"] = \"normal\"\n) => {\n const icons = {\n poison: {\n weak: \"๐Ÿงช\",\n normal: \"โ˜ ๏ธ\",\n strong: \"๐Ÿ’€\",\n },\n freeze: {\n weak: \"โ„๏ธ\",\n normal: \"๐ŸงŠ\",\n strong: \"โ›„\",\n },\n burn: {\n weak: \"๐Ÿ”ฅ\",\n normal: \"๐Ÿ”ฅ\",\n strong: \"๐ŸŒ‹\",\n },\n stun: {\n weak: \"๐Ÿ’ซ\",\n normal: \"โญ\",\n strong: \"โœจ\",\n },\n heal: {\n weak: \"๐Ÿ’š\",\n normal: \"โค๏ธ\",\n strong: \"๐Ÿ’—\",\n },\n shield: {\n weak: \"๐Ÿ›ก๏ธ\",\n normal: \"๐Ÿ”ฐ\",\n strong: \"โš”๏ธ\",\n },\n speed: {\n weak: \"๐Ÿ’จ\",\n normal: \"โšก\",\n strong: \"๐Ÿƒ\",\n },\n slow: {\n weak: \"๐ŸŒ\",\n normal: \"โณ\",\n strong: \"๐Ÿ•ฐ๏ธ\",\n },\n };\n\n return icons[type][intensity];\n};\n\n// Get status effect colors\nconst getStatusEffectColor = (type: StatusEffect[\"type\"], intensity: StatusEffect[\"intensity\"] = \"normal\") => {\n const colors = {\n poison: {\n weak: \"bg-green-500/20 border-green-400 text-green-300\",\n normal: \"bg-green-600/30 border-green-500 text-green-200\",\n strong: \"bg-green-700/40 border-green-600 text-green-100\",\n },\n freeze: {\n weak: \"bg-blue-500/20 border-blue-400 text-blue-300\",\n normal: \"bg-blue-600/30 border-blue-500 text-blue-200\",\n strong: \"bg-blue-700/40 border-blue-600 text-blue-100\",\n },\n burn: {\n weak: \"bg-red-500/20 border-red-400 text-red-300\",\n normal: \"bg-red-600/30 border-red-500 text-red-200\",\n strong: \"bg-red-700/40 border-red-600 text-red-100\",\n },\n stun: {\n weak: \"bg-yellow-500/20 border-yellow-400 text-yellow-300\",\n normal: \"bg-yellow-600/30 border-yellow-500 text-yellow-200\",\n strong: \"bg-yellow-700/40 border-yellow-600 text-yellow-100\",\n },\n heal: {\n weak: \"bg-emerald-500/20 border-emerald-400 text-emerald-300\",\n normal: \"bg-emerald-600/30 border-emerald-500 text-emerald-200\",\n strong: \"bg-emerald-700/40 border-emerald-600 text-emerald-100\",\n },\n shield: {\n weak: \"bg-slate-500/20 border-slate-400 text-slate-300\",\n normal: \"bg-slate-600/30 border-slate-500 text-slate-200\",\n strong: \"bg-slate-700/40 border-slate-600 text-slate-100\",\n },\n speed: {\n weak: \"bg-cyan-500/20 border-cyan-400 text-cyan-300\",\n normal: \"bg-cyan-600/30 border-cyan-500 text-cyan-200\",\n strong: \"bg-cyan-700/40 border-cyan-600 text-cyan-100\",\n },\n slow: {\n weak: \"bg-orange-500/20 border-orange-400 text-orange-300\",\n normal: \"bg-orange-600/30 border-orange-500 text-orange-200\",\n strong: \"bg-orange-700/40 border-orange-600 text-orange-100\",\n },\n };\n\n return colors[type][intensity];\n};\n\nfunction StatusEffectBadge({\n effect,\n size,\n showDuration,\n showStacks,\n animated,\n}: {\n effect: StatusEffect;\n size: VariantProps[\"size\"];\n showDuration?: boolean;\n showStacks?: boolean;\n animated?: boolean;\n}) {\n const icon = getStatusEffectIcon(effect.type, effect.intensity);\n const colorClass = getStatusEffectColor(effect.type, effect.intensity);\n\n return (\n
\n {/* Main effect icon */}\n \n {icon}\n
\n\n {/* Duration indicator */}\n {showDuration && effect.duration && (\n
\n \n {effect.duration}\n \n
\n )}\n\n {/* Stack indicator */}\n {showStacks && effect.stacks && effect.stacks > 1 && (\n
\n \n {effect.stacks}\n \n
\n )}\n
\n );\n}\n\nexport function StatusEffectIndicator({\n effects,\n showDuration = true,\n showStacks = true,\n maxEffects = 10,\n className,\n variant,\n size = \"md\",\n animated = true,\n ...props\n}: StatusEffectIndicatorProps) {\n const visibleEffects = React.useMemo(() => {\n return effects.slice(0, maxEffects);\n }, [effects, maxEffects]);\n\n const remainingCount = effects.length - maxEffects;\n\n if (effects.length === 0) {\n return null;\n }\n\n return (\n \n {visibleEffects.map((effect) => (\n \n ))}\n\n {/* Show remaining count if there are more effects */}\n {remainingCount > 0 && (\n 1 ? \"s\" : \"\"}`}\n >\n +{remainingCount}\n
\n )}\n \n );\n}\n\nexport default StatusEffectIndicator;\n", + "type": "registry:component", + "target": "components/ui/8bit/status-effect-indicator.tsx" + }, + { + "path": "components/ui/8bit/styles/retro.css", + "content": "@import url(\"https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap\");\n\n.retro {\n font-family:\n \"Press Start 2P\",\n system-ui,\n -apple-system,\n sans-serif;\n line-height: 1.5;\n letter-spacing: 0.5px;\n}\n", + "type": "registry:component", + "target": "components/ui/8bit/styles/retro.css" + } + ] +} \ No newline at end of file diff --git a/registry.json b/registry.json index 0a9138c7..aaeacd43 100644 --- a/registry.json +++ b/registry.json @@ -1487,6 +1487,25 @@ "target": "components/ui/8bit/separator.tsx" } ] + }, + { + "name": "status-effect-indicator", + "type": "registry:component", + "title": "8-bit Status Effect Indicator", + "description": "A retro-styled status effect indicator component for displaying pixel art icons of various game status effects.", + "registryDependencies": [], + "files": [ + { + "path": "components/ui/8bit/status-effect-indicator.tsx", + "type": "registry:component", + "target": "components/ui/8bit/status-effect-indicator.tsx" + }, + { + "path": "components/ui/8bit/styles/retro.css", + "type": "registry:component", + "target": "components/ui/8bit/styles/retro.css" + } + ] } ] }