Files
lytsup-site/src/components/games/CoinCatcher.tsx
R-Bharathraj 3cb2613195 first commit
2026-07-15 17:13:09 +05:30

392 lines
14 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import { useUser } from "@/lib/context";
import { Play, RotateCcw, Coins, ShieldAlert, Award, Heart } from "lucide-react";
interface Particle {
x: number;
y: number;
vx: number;
vy: number;
color: string;
alpha: number;
size: number;
}
interface GameObject {
x: number;
y: number;
size: number;
speed: number;
type: "coin" | "obstacle";
}
export default function CoinCatcher() {
const { addLyts } = useUser();
const canvasRef = useRef<HTMLCanvasElement>(null);
// Game states
const [gameState, setGameState] = useState<"idle" | "playing" | "gameover">("idle");
const [score, setScore] = useState(0);
const [lives, setLives] = useState(3);
const [claimed, setClaimed] = useState(false);
// References for mutable game loop variables
const requestRef = useRef<number>(0);
const objectsRef = useRef<GameObject[]>([]);
const particlesRef = useRef<Particle[]>([]);
const playerXRef = useRef<number>(200);
const livesRef = useRef<number>(3);
const scoreRef = useRef<number>(0);
const spawnTimerRef = useRef<number>(0);
// Start the game
const startGame = () => {
setGameState("playing");
setScore(0);
setLives(3);
setClaimed(false);
livesRef.current = 3;
scoreRef.current = 0;
objectsRef.current = [];
particlesRef.current = [];
playerXRef.current = 200;
};
// Claim score as LYTs
const claimEarnings = () => {
if (score > 0 && !claimed) {
addLyts(score);
setClaimed(true);
}
};
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
// Track mouse coordinates to control player position
const handleMouseMove = (e: MouseEvent) => {
const rect = canvas.getBoundingClientRect();
const relativeX = e.clientX - rect.left;
// Clamp player position within canvas bounds
playerXRef.current = Math.max(30, Math.min(canvas.width - 30, relativeX));
};
// Track touch events for mobile support
const handleTouchMove = (e: TouchEvent) => {
if (e.touches.length === 0) return;
const rect = canvas.getBoundingClientRect();
const relativeX = e.touches[0].clientX - rect.left;
playerXRef.current = Math.max(30, Math.min(canvas.width - 30, relativeX));
};
canvas.addEventListener("mousemove", handleMouseMove);
canvas.addEventListener("touchmove", handleTouchMove);
// Particle emitter for explosion effects
const createExplosion = (x: number, y: number, color: string) => {
for (let i = 0; i < 15; i++) {
particlesRef.current.push({
x,
y,
vx: (Math.random() - 0.5) * 6,
vy: (Math.random() - 0.5) * 6 - 2,
color,
alpha: 1,
size: Math.random() * 3 + 2,
});
}
};
// Main physics and rendering game loop
const gameLoop = () => {
if (gameState !== "playing") return;
// Clear Canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 1. Draw Catcher Bucket (Player)
const pX = playerXRef.current;
const pY = canvas.height - 35;
// Outer glow for player
ctx.shadowBlur = 15;
ctx.shadowColor = "#ffc72c";
ctx.fillStyle = "#ffc72c";
ctx.beginPath();
// Draw a sleek curved basket shape
ctx.roundRect(pX - 35, pY, 70, 15, [4, 4, 10, 10]);
ctx.fill();
// Inside mesh glow
ctx.shadowBlur = 5;
ctx.shadowColor = "#ff2e88";
ctx.fillStyle = "#ff2e88";
ctx.fillRect(pX - 25, pY + 4, 50, 2);
ctx.shadowBlur = 0; // Reset shadow
// 2. Spawn Items
spawnTimerRef.current += 1;
if (spawnTimerRef.current > 40) {
spawnTimerRef.current = 0;
const type = Math.random() > 0.35 ? "coin" : "obstacle";
objectsRef.current.push({
x: Math.random() * (canvas.width - 40) + 20,
y: -20,
size: type === "coin" ? 12 : 14,
speed: Math.random() * 2.5 + 3 + scoreRef.current * 0.05, // Speed increases as score goes up
type,
});
}
// 3. Update and Draw Falling Items
const activeObjects: GameObject[] = [];
objectsRef.current.forEach((obj) => {
obj.y += obj.speed;
// Collision Check (AABB / Circle collision against basket)
const hitPlayer =
obj.y + obj.size >= pY &&
obj.y - obj.size <= pY + 15 &&
obj.x >= pX - 45 &&
obj.x <= pX + 45;
if (hitPlayer) {
if (obj.type === "coin") {
scoreRef.current += 1;
setScore(scoreRef.current);
createExplosion(obj.x, obj.y, "#ffc72c");
} else {
livesRef.current -= 1;
setLives(livesRef.current);
createExplosion(obj.x, obj.y, "#ff2e88");
// Screen shake effect on bomb hit
ctx.translate((Math.random() - 0.5) * 15, (Math.random() - 0.5) * 15);
if (livesRef.current <= 0) {
setGameState("gameover");
}
}
} else if (obj.y < canvas.height + 20) {
activeObjects.push(obj);
}
});
objectsRef.current = activeObjects;
// Draw active falling objects
objectsRef.current.forEach((obj) => {
ctx.shadowBlur = 12;
if (obj.type === "coin") {
// Yellow LYT Coin
ctx.shadowColor = "#ffc72c";
ctx.fillStyle = "#ffc72c";
ctx.beginPath();
ctx.arc(obj.x, obj.y, obj.size, 0, Math.PI * 2);
ctx.fill();
// Outer yellow shine ring
ctx.strokeStyle = "#ffffff";
ctx.lineWidth = 1.5;
ctx.stroke();
// Core details
ctx.fillStyle = "#ffffff";
ctx.font = "bold 9px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("L", obj.x, obj.y);
} else {
// Purple Obstacle Spike
ctx.shadowColor = "#8b3dff";
ctx.fillStyle = "#8b3dff";
ctx.beginPath();
// Draw simple triangle obstacle
ctx.moveTo(obj.x, obj.y - obj.size);
ctx.lineTo(obj.x + obj.size, obj.y + obj.size);
ctx.lineTo(obj.x - obj.size, obj.y + obj.size);
ctx.closePath();
ctx.fill();
// Spiky border
ctx.strokeStyle = "#ff2e88";
ctx.lineWidth = 1;
ctx.stroke();
}
});
ctx.shadowBlur = 0;
// 4. Update and Draw Particle Explosions
particlesRef.current.forEach((p, idx) => {
p.x += p.vx;
p.y += p.vy;
p.alpha -= 0.025;
ctx.fillStyle = p.color;
ctx.globalAlpha = Math.max(0, p.alpha);
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
});
ctx.globalAlpha = 1.0;
particlesRef.current = particlesRef.current.filter((p) => p.alpha > 0);
// Loop
requestRef.current = requestAnimationFrame(gameLoop);
};
if (gameState === "playing") {
requestRef.current = requestAnimationFrame(gameLoop);
}
return () => {
cancelAnimationFrame(requestRef.current);
canvas.removeEventListener("mousemove", handleMouseMove);
canvas.removeEventListener("touchmove", handleTouchMove);
};
}, [gameState]);
return (
<section id="play" className="py-24 bg-[#060606] relative overflow-hidden">
{/* Background glow */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] rounded-full bg-accent-pink/5 blur-[120px] pointer-events-none" />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
{/* Header */}
<div className="text-center max-w-3xl mx-auto mb-16 space-y-4">
<span className="text-accent-lime font-extrabold text-xs tracking-widest uppercase">
Play & Earn
</span>
<h2 className="text-3xl sm:text-5xl font-extrabold font-sora text-white leading-tight">
The <span className="text-glow-yellow text-primary">LYT Catcher</span> Mini-Game
</h2>
<p className="text-white/60 text-base sm:text-lg">
Use your mouse or touch screen to catch the falling yellow LYT coins. Avoid the purple hazard spikes! Points are credited straight to your wallet.
</p>
</div>
{/* Game Box Container */}
<div className="max-w-xl mx-auto">
<div className="relative rounded-3xl overflow-hidden bg-[#0a0a0a] border border-white/5 shadow-[0_20px_50px_rgba(0,0,0,0.8)] aspect-[4/5] sm:aspect-[4/5] w-full">
{/* Top Game UI Bar (While Playing) */}
{gameState === "playing" && (
<div className="absolute top-4 left-4 right-4 flex justify-between items-center z-10">
{/* Score */}
<div className="flex items-center space-x-1.5 px-3 py-1.5 rounded-full bg-black/50 border border-white/10 text-primary font-bold text-sm">
<Coins className="w-4 h-4 text-primary animate-pulse" />
<span>Score: {score}</span>
</div>
{/* Lives */}
<div className="flex items-center space-x-1 px-3 py-1.5 rounded-full bg-black/50 border border-white/10 text-red-500 font-semibold text-sm">
{Array.from({ length: 3 }).map((_, i) => (
<Heart
key={i}
className={`w-4 h-4 fill-current ${
i < lives ? "text-red-500" : "text-white/20"
}`}
/>
))}
</div>
</div>
)}
{/* Canvas */}
<canvas
ref={canvasRef}
width={480}
height={600}
className="w-full h-full block bg-radial-gradient"
/>
{/* Screens overlays */}
{/* 1. Idle Overlay */}
{gameState === "idle" && (
<div className="absolute inset-0 bg-dark/90 flex flex-col items-center justify-center p-6 text-center space-y-6">
<div className="w-16 h-16 rounded-3xl bg-primary/10 border border-primary/20 flex items-center justify-center text-primary shadow-[0_0_20px_rgba(255,199,44,0.1)]">
<Coins className="w-8 h-8 text-primary animate-bounce" />
</div>
<div className="space-y-2">
<h3 className="text-2xl font-bold font-sora text-white">Ready to Catch?</h3>
<p className="text-white/60 text-xs max-w-xs leading-relaxed">
Move your mouse or finger horizontally to control the catcher basket. Grab coins, avoid spikes!
</p>
</div>
<button
onClick={startGame}
className="px-8 py-3.5 rounded-2xl bg-primary text-dark font-extrabold text-sm tracking-wider flex items-center justify-center gap-2 shadow-[0_0_25px_rgba(255,199,44,0.3)] hover:scale-105 transition-transform cursor-pointer"
>
<Play className="w-4.5 h-4.5 fill-current" />
<span>Start Game</span>
</button>
</div>
)}
{/* 2. Gameover Overlay */}
{gameState === "gameover" && (
<div className="absolute inset-0 bg-dark/95 flex flex-col items-center justify-center p-6 text-center space-y-8 animate-in fade-in duration-300">
<div className="w-16 h-16 rounded-3xl bg-red-500/10 border border-red-500/20 flex items-center justify-center text-red-500">
<ShieldAlert className="w-8 h-8" />
</div>
<div className="space-y-2">
<h3 className="text-3xl font-extrabold font-sora text-white">Game Over</h3>
<p className="text-white/60 text-sm">
You caught <span className="text-primary font-bold text-lg">{score}</span> LYTs!
</p>
</div>
{/* Claim details */}
<div className="w-full max-w-xs space-y-3">
<button
onClick={claimEarnings}
disabled={score === 0 || claimed}
className={`w-full py-4 rounded-2xl font-extrabold text-sm tracking-wide flex items-center justify-center gap-2 cursor-pointer transition-all ${
claimed
? "bg-accent-lime text-dark shadow-[0_0_20px_rgba(200,255,55,0.3)]"
: score === 0
? "bg-white/5 text-white/20 border border-white/5 cursor-not-allowed"
: "bg-primary text-dark shadow-[0_0_25px_rgba(255,199,44,0.3)]"
}`}
>
{claimed ? (
<>
<Award className="w-4.5 h-4.5" />
<span>+{score} LYTs Claimed!</span>
</>
) : (
<>
<Coins className="w-4.5 h-4.5" />
<span>Claim Reward (+{score} LYTs)</span>
</>
)}
</button>
<button
onClick={startGame}
className="w-full py-3.5 rounded-2xl bg-white/5 hover:bg-white/10 border border-white/10 text-white font-bold text-sm tracking-wide cursor-pointer flex items-center justify-center gap-2 transition-all"
>
<RotateCcw className="w-4 h-4" />
<span>Play Again</span>
</button>
</div>
</div>
)}
</div>
</div>
</div>
</section>
);
}