Files

516 lines
24 KiB
TypeScript

"use client";
import { useState, useRef, useEffect, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
type GameState = "upload" | "ready" | "playing" | "gameover";
type BossState = "idle" | "attacking" | "stunned" | "dizzy";
const powerUps: Record<string, { name: string; icon: string; duration: number }> = {
double: { name: "2x Damage", icon: "⚔️", duration: 5000 },
freeze: { name: "Stun Boss", icon: "❄️", duration: 3000 },
bomb: { name: "Bomb", icon: "💣", duration: 0 },
none: { name: "", icon: "", duration: 0 },
};
const playSound = (type: "punch" | "combo" | "powerup" | "ko" | "gameover" | "bossAttack" | "hit") => {
try {
const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
switch (type) {
case "punch":
oscillator.frequency.setValueAtTime(150, audioCtx.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(50, audioCtx.currentTime + 0.1);
gainNode.gain.setValueAtTime(0.5, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.1);
break;
case "hit":
oscillator.frequency.setValueAtTime(200, audioCtx.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(80, audioCtx.currentTime + 0.15);
gainNode.gain.setValueAtTime(0.4, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.15);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.15);
break;
case "bossAttack":
oscillator.frequency.setValueAtTime(80, audioCtx.currentTime);
oscillator.frequency.setValueAtTime(120, audioCtx.currentTime + 0.1);
oscillator.frequency.setValueAtTime(80, audioCtx.currentTime + 0.2);
gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.3);
break;
case "combo":
oscillator.frequency.setValueAtTime(300, audioCtx.currentTime);
oscillator.frequency.setValueAtTime(400, audioCtx.currentTime + 0.1);
oscillator.frequency.setValueAtTime(500, audioCtx.currentTime + 0.2);
gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.3);
break;
case "powerup":
oscillator.frequency.setValueAtTime(200, audioCtx.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(800, audioCtx.currentTime + 0.3);
gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.3);
break;
case "ko":
oscillator.frequency.setValueAtTime(100, audioCtx.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(30, audioCtx.currentTime + 0.5);
gainNode.gain.setValueAtTime(0.8, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.5);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.5);
break;
case "gameover":
oscillator.frequency.setValueAtTime(400, audioCtx.currentTime);
oscillator.frequency.setValueAtTime(300, audioCtx.currentTime + 0.2);
oscillator.frequency.setValueAtTime(200, audioCtx.currentTime + 0.4);
gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.6);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.6);
break;
}
} catch (e) {}
};
export default function BossPunch() {
const [bossImage, setBossImage] = useState<string | null>(null);
const [gameState, setGameState] = useState<GameState>("upload");
const [score, setScore] = useState(0);
const [bossHealth, setBossHealth] = useState(100);
const [playerHealth, setPlayerHealth] = useState(100);
const [combo, setCombo] = useState(0);
const [maxCombo, setMaxCombo] = useState(0);
const [lastHit, setLastHit] = useState<"left" | "right" | null>(null);
const [playerX, setPlayerX] = useState(0);
const [bossX, setBossX] = useState(0);
const [bossRotation, setBossRotation] = useState(0);
const [bossState, setBossState] = useState<BossState>("idle");
const [isPaid, setIsPaid] = useState(false);
const [showPayModal, setShowPayModal] = useState(false);
const [activePowerUp, setActivePowerUp] = useState<string>("none");
const [powerUpCooldown, setPowerUpCooldown] = useState(0);
const [floatingTexts, setFloatingTexts] = useState<{ id: number; text: string; x: number; y: number; type: string }[]>([]);
const [screenShake, setScreenShake] = useState(false);
const [particles, setParticles] = useState<{ id: number; x: number; y: number; vx: number; vy: number; color: string; life: number }[]>([]);
const [winQuote, setWinQuote] = useState("I am sorry Haitham!");
const [showWinQuoteInput, setShowWinQuoteInput] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const floatingIdRef = useRef(0);
const particleIdRef = useRef(0);
const attackIntervalRef = useRef<NodeJS.Timeout | null>(null);
const addFloatingText = (text: string, x: number, y: number, type: string = "damage") => {
const id = floatingIdRef.current++;
setFloatingTexts(prev => [...prev, { id, text, x, y, type }]);
setTimeout(() => {
setFloatingTexts(prev => prev.filter(t => t.id !== id));
}, 1500);
};
const addParticle = (x: number, y: number, color: string) => {
const id = particleIdRef.current++;
setParticles(prev => [...prev, {
id, x, y,
vx: (Math.random() - 0.5) * 10,
vy: (Math.random() - 0.5) * 10 - 5,
color, life: 1
}]);
};
useEffect(() => {
if (particles.length > 0) {
const timer = setInterval(() => {
setParticles(prev => prev.map(p => ({
...p,
x: p.x + p.vx,
y: p.y + p.vy,
vy: p.vy + 0.5,
life: p.life - 0.05
})).filter(p => p.life > 0));
}, 16);
return () => clearInterval(timer);
}
}, [particles.length]);
useEffect(() => {
if (gameState === "playing" && bossState !== "stunned" && bossState !== "dizzy") {
attackIntervalRef.current = setInterval(() => {
if (bossState === "idle" && Math.random() < 0.3) {
bossAttack();
}
}, 2000);
}
return () => {
if (attackIntervalRef.current) clearInterval(attackIntervalRef.current);
};
}, [gameState, bossState]);
const bossAttack = () => {
if (bossState !== "idle") return;
setBossState("attacking");
playSound("bossAttack");
setBossX(-30);
setTimeout(() => {
setBossX(30);
setTimeout(() => {
const damage = 5 + Math.floor(Math.random() * 10);
setPlayerHealth(prev => Math.max(0, prev - damage));
playSound("hit");
setScreenShake(true);
setTimeout(() => setScreenShake(false), 200);
addFloatingText(`-${damage}`, 30, 60, "boss");
setBossX(0);
setBossState("idle");
if (playerHealth - damage <= 0) {
setGameState("gameover");
playSound("gameover");
}
}, 150);
}, 150);
};
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
setBossImage(event.target?.result as string);
setGameState("ready");
};
reader.readAsDataURL(file);
}
};
const startGame = () => {
setGameState("playing");
setScore(0);
setBossHealth(100);
setPlayerHealth(100);
setCombo(0);
setMaxCombo(0);
setActivePowerUp("none");
setBossState("idle");
};
const activatePowerUp = (type: string) => {
if (powerUpCooldown > 0 || gameState !== "playing") return;
playSound("powerup");
setActivePowerUp(type);
if (type === "bomb") {
setBossHealth(prev => Math.max(0, prev - 30));
addFloatingText("-30! 💣", 70, 30, "powerup");
setScreenShake(true);
setTimeout(() => setScreenShake(false), 300);
} else if (type === "freeze") {
setBossState("stunned");
setTimeout(() => setBossState("idle"), 3000);
} else {
setTimeout(() => setActivePowerUp("none"), powerUps[type]?.duration || 5000);
}
setPowerUpCooldown(10);
};
useEffect(() => {
if (powerUpCooldown > 0) {
const timer = setInterval(() => {
setPowerUpCooldown(prev => Math.max(0, prev - 1));
}, 1000);
return () => clearInterval(timer);
}
}, [powerUpCooldown]);
const punch = useCallback((hand: "left" | "right") => {
if (gameState !== "playing" || bossState === "stunned") return;
if (Math.random() < 0.15 && bossState === "idle") {
setBossState("dizzy");
addFloatingText("Miss!", 70, 40, "miss");
setTimeout(() => setBossState("idle"), 500);
return;
}
setLastHit(hand);
setCombo(c => {
const newCombo = c + 1;
if (newCombo > maxCombo) setMaxCombo(newCombo);
if (newCombo % 10 === 0) {
playSound("combo");
addFloatingText(`${newCombo} COMBO! 🔥`, 50, 50, "combo");
}
return newCombo;
});
let damage = 5 + Math.floor(combo / 5);
damage = Math.min(damage, 20);
if (activePowerUp === "double") damage *= 2;
playSound("punch");
const newHealth = Math.max(0, bossHealth - damage);
setBossHealth(newHealth);
setScore(s => s + damage * 10);
setBossRotation(hand === "left" ? -20 - (damage * 2) : 20 + (damage * 2));
setBossX(hand === "left" ? -20 : 20);
setTimeout(() => { setBossRotation(0); setBossX(0); }, 150);
setPlayerX(hand === "left" ? 40 : -40);
setTimeout(() => setPlayerX(0), 100);
for (let i = 0; i < 5; i++) {
addParticle(70, 40, damage > 15 ? "#ff0000" : "#ffffff");
}
addFloatingText(`-${damage}`, 60 + (Math.random() * 20 - 10), 30 + (Math.random() * 20 - 10), "damage");
if (damage > 10) {
setScreenShake(true);
setTimeout(() => setScreenShake(false), 100);
}
setTimeout(() => setLastHit(null), 150);
if (newHealth <= 0) {
playSound("ko");
setGameState("gameover");
}
}, [gameState, combo, bossHealth, activePowerUp, maxCombo, bossState]);
const resetGame = () => {
setGameState("ready");
setScore(0);
setBossHealth(100);
setPlayerHealth(100);
setCombo(0);
setMaxCombo(0);
setActivePowerUp("none");
setBossState("idle");
};
const shakeClass = screenShake ? "animate-pulse" : "";
return (
<div className={`min-h-screen bg-gradient-to-b from-red-900 via-red-800 to-orange-900 flex flex-col items-center justify-center p-2 overflow-hidden ${shakeClass}`}>
{particles.map(p => (
<motion.div
key={p.id}
initial={{ x: `${p.x}%`, y: `${p.y}%`, opacity: 1 }}
animate={{ x: `${p.x + p.vx}%`, y: `${p.y + p.vy}%`, opacity: p.life }}
className="absolute w-3 h-3 rounded-full pointer-events-none"
style={{ backgroundColor: p.color }}
/>
))}
{floatingTexts.map(ft => (
<motion.div
key={ft.id}
initial={{ opacity: 1, y: 0, scale: 0.5 }}
animate={{ opacity: 0, y: -50, scale: ft.type === "boss" ? 1.5 : 1.2 }}
transition={{ duration: 1 }}
className={`absolute pointer-events-none text-2xl font-black ${ft.type === "boss" ? "text-red-400" : ft.type === "miss" ? "text-gray-400" : ft.type === "combo" ? "text-yellow-400" : "text-white"}`}
style={{ left: `${ft.x}%`, top: `${ft.y}%` }}
>
{ft.text}
</motion.div>
))}
<div className="absolute top-0 left-0 right-0 p-2 flex justify-between items-center z-10">
<h1 className="text-xl font-black text-white tracking-wider">
BOSS<span className="text-red-500">PUNCH</span>
</h1>
{gameState === "playing" && (
<div className="bg-black/50 rounded-full px-3 py-1 flex gap-3 text-xs">
<span className="text-white font-bold">Score: {score}</span>
<span className="text-yellow-400 font-bold">Combo: {combo}x</span>
</div>
)}
</div>
{gameState === "playing" && (
<div className="absolute top-12 left-2 right-2 flex flex-col gap-1 z-10">
<div className="flex items-center gap-2">
<span className="text-xs text-white w-12">BOSS</span>
<div className="flex-1 h-3 bg-gray-800 rounded-full overflow-hidden">
<div className="h-full bg-gradient-to-r from-red-500 to-green-500 transition-all duration-200" style={{ width: `${bossHealth}%` }} />
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-white w-12">YOU</span>
<div className="flex-1 h-3 bg-gray-800 rounded-full overflow-hidden">
<div className="h-full bg-gradient-to-r from-green-500 to-blue-500 transition-all duration-200" style={{ width: `${playerHealth}%` }} />
</div>
</div>
</div>
)}
{gameState === "playing" && (
<div className="absolute top-24 left-0 right-0 flex justify-center gap-2 z-10">
{(["double", "freeze", "bomb"] as string[]).map(pu => (
<button
key={pu}
onClick={() => activatePowerUp(pu)}
disabled={powerUpCooldown > 0 || bossState === "stunned"}
className={`bg-white/20 backdrop-blur rounded-full p-2 text-xl transition-all hover:scale-110 disabled:opacity-50 disabled:cursor-not-allowed ${activePowerUp === pu ? "ring-2 ring-yellow-400" : ""}`}
title={powerUps[pu]?.name}
>
{powerUps[pu]?.icon}
</button>
))}
{powerUpCooldown > 0 && <span className="absolute -bottom-2 text-xs text-white/70">{powerUpCooldown}s</span>}
</div>
)}
<div className="relative w-full max-w-lg aspect-square max-h-[45vh] mt-8">
<AnimatePresence>
{bossImage && (
<motion.div
className="absolute right-0 top-1/3"
animate={{
x: bossX,
rotate: bossState === "dizzy" ? [0, -10, 10, -10, 10, 0] : bossRotation,
scale: bossState === "stunned" ? 0.9 : bossHealth > 0 ? 1 : 0.8,
opacity: bossState === "stunned" ? 0.7 : 1
}}
transition={{ type: "spring", stiffness: 300 }}
>
<div className="relative w-32 h-32 md:w-48 md:h-48">
<div className={`absolute -left-6 top-1/2 w-10 h-10 ${activePowerUp === "freeze" ? "bg-cyan-300" : bossState === "dizzy" ? "bg-gray-400" : "bg-red-600"} rounded-full border-4 border-red-400 flex items-center justify-center`}>
<span className="text-white font-bold text-xs">L</span>
</div>
<div className={`absolute -right-6 top-1/3 w-10 h-10 ${activePowerUp === "freeze" ? "bg-cyan-300" : bossState === "dizzy" ? "bg-gray-400" : "bg-red-600"} rounded-full border-4 border-red-400 flex items-center justify-center`}>
<span className="text-white font-bold text-xs">R</span>
</div>
<div className={`w-full h-full rounded-2xl overflow-hidden border-4 ${activePowerUp === "freeze" ? "border-cyan-400" : bossState === "stunned" ? "border-blue-400" : "border-white"} shadow-2xl`}>
<img src={bossImage} alt="Boss" className="w-full h-full object-cover" />
</div>
{bossState === "stunned" && <div className="absolute -top-4 left-1/2 -translate-x-1/2 text-2xl">💫</div>}
{bossState === "dizzy" && <div className="absolute top-0 right-0 text-xl">😵</div>}
</div>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{gameState === "playing" && (
<motion.div className="absolute left-0 bottom-0" animate={{ x: playerX }} transition={{ duration: 0.1 }}>
<div className="w-20 h-28 md:w-28 md:h-36 relative">
<motion.div className="absolute top-1/3 -right-3 w-12 h-12 bg-blue-600 border-4 border-blue-400 rounded-full cursor-pointer flex items-center justify-center text-2xl" whileTap={{ scale: 0.8 }} onClick={() => punch("right")}>👊</motion.div>
<motion.div className="absolute top-1/3 -left-3 w-12 h-12 bg-blue-600 border-4 border-blue-400 rounded-full cursor-pointer flex items-center justify-center text-2xl" whileTap={{ scale: 0.8 }} onClick={() => punch("left")}>👊</motion.div>
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-16 h-20 bg-blue-700 rounded-t-xl" />
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-10 h-10 bg-amber-200 rounded-full" />
</div>
</motion.div>
)}
</AnimatePresence>
{lastHit && (
<motion.div initial={{ opacity: 1, scale: 0.5 }} animate={{ opacity: 0, scale: 1.5 }} className="absolute right-1/4 top-1/3 text-5xl font-black text-yellow-400">
Pow!
</motion.div>
)}
</div>
<div className="mt-2 w-full max-w-md">
{gameState === "upload" && (
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="text-center">
<div className="bg-white/10 backdrop-blur rounded-2xl p-6 border-2 border-dashed border-white/30">
<div className="text-5xl mb-3">👔</div>
<h2 className="text-xl font-bold text-white mb-2">Upload Your Boss!</h2>
<p className="text-white/70 mb-4">Take out your frustrations!</p>
<input ref={fileInputRef} type="file" accept="image/*" onChange={handleImageUpload} className="hidden" />
<button onClick={() => fileInputRef.current?.click()} className="bg-red-500 hover:bg-red-600 text-white font-bold py-3 px-8 rounded-full text-lg transition-all hover:scale-105 shadow-lg">
📸 Upload Photo
</button>
<p className="text-xs text-white/50 mt-4">Free with ads $9.99 for ad-free</p>
</div>
</motion.div>
)}
{gameState === "ready" && bossImage && (
<motion.div initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} className="text-center">
<div className="bg-white/10 backdrop-blur rounded-xl p-3 mb-3 border border-white/20">
<button onClick={() => setShowWinQuoteInput(!showWinQuoteInput)} className="text-white/70 text-sm hover:text-white flex items-center justify-center gap-2">
🏆 {showWinQuoteInput ? "Hide" : "If u win he says..."}
</button>
{showWinQuoteInput && (
<input
type="text"
value={winQuote}
onChange={(e) => setWinQuote(e.target.value)}
placeholder="What does boss say when u win?"
className="w-full mt-2 bg-white/20 border border-white/30 rounded-lg px-3 py-2 text-white placeholder-white/50 text-sm"
/>
)}
</div>
<h2 className="text-xl font-bold text-white mb-2">Ready to Fight!</h2>
<p className="text-white/70 mb-4">Boss fights back! Don't lose!</p>
<button onClick={startGame} className="bg-gradient-to-r from-red-500 to-orange-500 hover:from-red-600 hover:to-orange-600 text-white font-bold py-3 px-10 rounded-full text-lg transition-all hover:scale-105 shadow-xl">
🥊 START FIGHT
</button>
</motion.div>
)}
{gameState === "playing" && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center">
<p className="text-white/70 text-sm mb-2">Tap to punch!</p>
<div className="flex gap-4 justify-center">
<button onClick={() => punch("left")} className="bg-blue-600 hover:scale-110 w-20 h-20 rounded-full border-4 border-blue-400 flex items-center justify-center text-3xl active:scale-90 transition-transform">👊</button>
<button onClick={() => punch("right")} className="bg-blue-600 hover:scale-110 w-20 h-20 rounded-full border-4 border-blue-400 flex items-center justify-center text-3xl active:scale-90 transition-transform">👊</button>
</div>
</motion.div>
)}
{gameState === "gameover" && (
<motion.div initial={{ opacity: 0, scale: 0.5 }} animate={{ opacity: 1, scale: 1 }} className="text-center">
<div className={`rounded-2xl p-6 border-4 ${bossHealth <= 0 ? "bg-gradient-to-b from-yellow-500 to-orange-500 border-yellow-300" : "bg-gradient-to-b from-red-600 to-red-800 border-red-400"}`}>
<div className="text-6xl mb-2">{bossHealth <= 0 ? "🏆" : "💀"}</div>
<h2 className="text-3xl font-black text-white mb-2">{bossHealth <= 0 ? "KNOCKOUT!" : "YOU LOSE!"}</h2>
{bossHealth <= 0 && (
<div className="bg-white/20 rounded-lg p-3 mb-3">
<p className="text-lg font-bold text-white">Boss says:</p>
<p className="text-white text-xl">"{winQuote}"</p>
</div>
)}
<p className="text-xl font-bold text-white mb-2">Score: {score}</p>
<p className="text-white/80 mb-4">Max Combo: {maxCombo}x</p>
<div className="space-y-2">
<button onClick={resetGame} className="w-full bg-white text-red-600 font-bold py-2 px-6 rounded-full text-base transition-all hover:scale-105">
🔄 Fight Again
</button>
{!isPaid && (
<button onClick={() => setShowPayModal(true)} className="w-full bg-yellow-400 text-yellow-900 font-bold py-2 px-6 rounded-full text-base transition-all hover:scale-105 flex items-center justify-center gap-2">
$9.99 - No Ads
</button>
)}
</div>
</div>
</motion.div>
)}
</div>
{/* Stripe Payment Modal */}
{showPayModal && (
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 max-w-md w-full text-center">
<h3 className="text-2xl font-bold text-gray-800 mb-2">Remove Ads</h3>
<p className="text-gray-600 mb-4">Get BossPunch ad-free forever!</p>
<p className="text-4xl font-black text-purple-600 mb-4">$9.99</p>
<button className="w-full bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 rounded-lg mb-2 transition">
Pay with Card
</button>
<button onClick={() => setShowPayModal(false)} className="text-gray-500 text-sm">Cancel</button>
</div>
</div>
)}
{!isPaid && gameState !== "upload" && (
<div className="fixed bottom-0 left-0 right-0 bg-black/80 py-2 text-center">
<p className="text-white/60 text-xs">Advertisement</p>
</div>
)}
</div>
);
}