Add LICENSE, README, and Docs tab to Mission Control
This commit is contained in:
@@ -0,0 +1,515 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user