import React, { useState, useEffect, useRef } from 'react'; import { Button } from '../components/ui/Button'; import { ArrowLeft, RefreshCw, Volume2, VolumeX, Trophy, Play, Zap, Shield } from 'lucide-react'; import { StorageService } from '../services/storageService'; interface GameProps { onExit: () => void; } export const DanpheRush: React.FC = ({ onExit }) => { const canvasRef = useRef(null); const audioCtxRef = useRef(null); const [gameState, setGameState] = useState<'READY' | 'PLAYING' | 'GAMEOVER'>('READY'); const [score, setScore] = useState(0); const [highScore, setHighScore] = useState(0); const [level, setLevel] = useState(1); const [audioEnabled, setAudioEnabled] = useState(true); // Boost State const [boostUnlocked, setBoostUnlocked] = useState(false); const [boostCharge, setBoostCharge] = useState(0); // 0 to 3 const [isBoosting, setIsBoosting] = useState(false); // Constants const GRAVITY = 0.4; const JUMP_STRENGTH = -7.5; const BOOST_STRENGTH = -12; const BASE_SPEED = 3.5; const PIPE_WIDTH = 70; const PIPE_GAP = 170; const LEVEL_THRESHOLD = 10; // Points per level const PIPES_FOR_BOOST = 3; const SPAWN_RATE = 120; const physics = useRef({ birdY: 200, velocity: 0, rotation: 0, pipes: [] as { x: number; topHeight: number; passed: boolean; broken?: boolean }[], clouds: [] as { x: number; y: number; scale: number; speed: number }[], particles: [] as { x: number; y: number; vx: number; vy: number; life: number; color: string }[], frame: 0, flashOpacity: 0, invincibleTimer: 0, speed: BASE_SPEED, pipesSinceBoost: 0 }); const requestRef = useRef(0); useEffect(() => { const loadData = async () => { const profile = await StorageService.getProfile(); if (profile?.highScores?.danphe) setHighScore(profile.highScores.danphe); }; loadData(); initClouds(); // Global Key Listener const handleKeyDown = (e: KeyboardEvent) => { if (e.code === 'Space') { e.preventDefault(); // Prevent scrolling flap(); } if (e.code === 'KeyE') { activateBoost(); } }; window.addEventListener('keydown', handleKeyDown); // Start loop requestRef.current = requestAnimationFrame(loop); return () => { cancelAnimationFrame(requestRef.current); window.removeEventListener('keydown', handleKeyDown); if (audioCtxRef.current) { audioCtxRef.current.close().catch(() => {}); } }; }, [gameState, boostUnlocked, boostCharge]); // Dependencies for key listener state access const initAudio = () => { if (!audioCtxRef.current) { const AudioContext = window.AudioContext || (window as any).webkitAudioContext; if (AudioContext) { audioCtxRef.current = new AudioContext(); } } if (audioCtxRef.current && audioCtxRef.current.state === 'suspended') { audioCtxRef.current.resume().catch(() => {}); } }; const initClouds = () => { physics.current.clouds = Array.from({ length: 6 }, () => ({ x: Math.random() * 800, y: Math.random() * 300, scale: 0.5 + Math.random() * 0.5, speed: 0.5 + Math.random() * 0.5 })); }; const playSound = (type: 'flap' | 'score' | 'hit' | 'boost') => { if (!audioEnabled) return; // Ensure context exists if (!audioCtxRef.current) initAudio(); const ctx = audioCtxRef.current; if (!ctx) return; const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); const now = ctx.currentTime; if (type === 'flap') { // Snappy Jump Sound osc.frequency.setValueAtTime(150, now); osc.frequency.linearRampToValueAtTime(300, now + 0.1); gain.gain.setValueAtTime(0.2, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1); osc.start(); osc.stop(now + 0.1); } else if (type === 'score') { osc.type = 'square'; osc.frequency.setValueAtTime(800, now); osc.frequency.setValueAtTime(1200, now + 0.05); gain.gain.setValueAtTime(0.1, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1); osc.start(); osc.stop(now + 0.1); } else if (type === 'hit') { osc.type = 'sawtooth'; osc.frequency.setValueAtTime(150, now); osc.frequency.linearRampToValueAtTime(100, now + 0.3); gain.gain.setValueAtTime(0.3, now); gain.gain.linearRampToValueAtTime(0.01, now + 0.3); osc.start(); osc.stop(now + 0.3); } else if (type === 'boost') { osc.type = 'sine'; osc.frequency.setValueAtTime(200, now); osc.frequency.linearRampToValueAtTime(800, now + 0.5); gain.gain.setValueAtTime(0.3, now); gain.gain.linearRampToValueAtTime(0.01, now + 0.5); osc.start(); osc.stop(now + 0.5); } }; const resetGame = () => { setGameState('READY'); setScore(0); setLevel(1); setBoostUnlocked(false); setBoostCharge(0); setIsBoosting(false); physics.current = { birdY: 250, velocity: 0, rotation: 0, pipes: [], clouds: physics.current.clouds, particles: [], frame: 0, flashOpacity: 0, invincibleTimer: 0, speed: BASE_SPEED, pipesSinceBoost: 0 }; }; const flap = () => { // Resume audio context on user interaction initAudio(); if (gameState === 'GAMEOVER') return; if (gameState === 'READY') { setGameState('PLAYING'); } physics.current.velocity = JUMP_STRENGTH; spawnParticles(100, physics.current.birdY, 'white'); playSound('flap'); }; const activateBoost = () => { // Resume audio context initAudio(); const P = physics.current; if (gameState !== 'PLAYING') return; const currentLevel = Math.floor(score / LEVEL_THRESHOLD) + 1; // Boost is ready if unlocked (Lvl 3+) AND charge is full const canBoost = currentLevel > 3 && P.pipesSinceBoost >= PIPES_FOR_BOOST; if (canBoost) { P.velocity = BOOST_STRENGTH; P.invincibleTimer = 60; // 1 second (at 60fps) P.pipesSinceBoost = 0; // Reset charge setBoostCharge(0); setIsBoosting(true); playSound('boost'); // Explosion effect for(let i=0; i<20; i++) { physics.current.particles.push({ x: 100, y: P.birdY, vx: (Math.random() - 0.5) * 10, vy: (Math.random() - 0.5) * 10, life: 1.0, color: '#facc15' }); } } }; const spawnParticles = (x: number, y: number, color: string) => { for(let i=0; i<5; i++) { physics.current.particles.push({ x, y, vx: (Math.random() - 0.5) * 3 - 2, // Move left mostly vy: (Math.random() - 0.5) * 3, life: 1.0, color }); } }; const gameOverLogic = () => { if (gameState === 'GAMEOVER') return; setGameState('GAMEOVER'); playSound('hit'); physics.current.flashOpacity = 1.0; if (score > highScore) { setHighScore(score); StorageService.saveHighScore('danphe', score); } // Updated addPoints StorageService.addPoints(score, score * 5, 'game_reward', 'Danphe Rush Score'); }; const loop = () => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; const WIDTH = canvas.width; const HEIGHT = canvas.height; const P = physics.current; // --- LOGIC --- // Level & Unlock Logic const currentLvl = Math.floor(score / LEVEL_THRESHOLD) + 1; if (currentLvl !== level) setLevel(currentLvl); const isUnlocked = currentLvl > 3; if (isUnlocked !== boostUnlocked) setBoostUnlocked(isUnlocked); // Sync React state for UI (limited updates to prevent lag) if (isUnlocked) { // Clamp charge visual const charge = Math.min(PIPES_FOR_BOOST, P.pipesSinceBoost); if (charge !== boostCharge) setBoostCharge(charge); } if (isBoosting && P.invincibleTimer <= 0) setIsBoosting(false); // Difficulty Scaling P.speed = BASE_SPEED + (currentLvl * 0.2); // Background movement (Clouds) if (gameState !== 'GAMEOVER') { P.clouds.forEach(c => { c.x -= c.speed * (gameState === 'PLAYING' ? 1 : 0.5); if (c.x < -100) c.x = WIDTH + 100; }); } if (gameState === 'PLAYING') { P.frame++; if (P.invincibleTimer > 0) P.invincibleTimer--; // Physics P.velocity += GRAVITY; P.birdY += P.velocity; // Rotation logic if (P.velocity < 0) { P.rotation = Math.max(-0.5, P.rotation - 0.1); } else { P.rotation = Math.min(Math.PI / 2, P.rotation + 0.05); } // Pipe Spawning if (P.frame % SPAWN_RATE === 0) { const minHeight = 100; const maxHeight = HEIGHT - 150 - PIPE_GAP; const height = Math.floor(Math.random() * (maxHeight - minHeight + 1)) + minHeight; P.pipes.push({ x: WIDTH, topHeight: height, passed: false }); } // Pipe Movement & Collision P.pipes.forEach(pipe => { pipe.x -= P.speed; // Collision AABB const birdLeft = 100 - 15; const birdRight = 100 + 15; const birdTop = P.birdY - 12; const birdBottom = P.birdY + 12; const pipeLeft = pipe.x; const pipeRight = pipe.x + PIPE_WIDTH; // Hit Pipe if (!pipe.broken && birdRight > pipeLeft && birdLeft < pipeRight) { if (birdTop < pipe.topHeight || birdBottom > pipe.topHeight + PIPE_GAP) { if (P.invincibleTimer > 0) { // Boost destroys pipe logic (visual only, we just mark it passed/broken) pipe.broken = true; spawnParticles(pipe.x, P.birdY, '#64748b'); // Stone debris setScore(s => s + 2); // Bonus points for smashing playSound('score'); } else { gameOverLogic(); } } } // Score & Charge if (!pipe.passed && birdLeft > pipeRight) { pipe.passed = true; setScore(s => s + 1); // Increment boost charge if (P.pipesSinceBoost < PIPES_FOR_BOOST) { P.pipesSinceBoost++; } playSound('score'); } }); // Cleanup pipes if (P.pipes.length > 0 && P.pipes[0].x < -PIPE_WIDTH) { P.pipes.shift(); } // Ground/Ceiling Collision if (P.birdY >= HEIGHT - 40 || P.birdY < 0) { if (P.invincibleTimer > 0 && P.birdY < 0) { // Hitting ceiling while boosting is fine, just clamp P.birdY = 0; P.velocity = 0; } else { gameOverLogic(); } } } else if (gameState === 'READY') { P.birdY = 250 + Math.sin(Date.now() / 300) * 10; P.rotation = 0; } else if (gameState === 'GAMEOVER') { if (P.birdY < HEIGHT - 40) { P.velocity += GRAVITY; P.birdY += P.velocity; P.rotation = Math.min(Math.PI / 2, P.rotation + 0.1); } } if (P.flashOpacity > 0) P.flashOpacity -= 0.05; // --- RENDER --- // Sky const skyGrad = ctx.createLinearGradient(0, 0, 0, HEIGHT); skyGrad.addColorStop(0, '#38bdf8'); // Sky blue skyGrad.addColorStop(1, '#bae6fd'); ctx.fillStyle = skyGrad; ctx.fillRect(0, 0, WIDTH, HEIGHT); // Mountains ctx.fillStyle = '#f1f5f9'; ctx.beginPath(); ctx.moveTo(0, HEIGHT); ctx.lineTo(200, HEIGHT - 200); ctx.lineTo(400, HEIGHT - 100); ctx.lineTo(600, HEIGHT - 250); ctx.lineTo(800, HEIGHT); ctx.fill(); // Clouds ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; P.clouds.forEach(c => { ctx.beginPath(); ctx.arc(c.x, c.y, 30 * c.scale, 0, Math.PI * 2); ctx.arc(c.x + 20 * c.scale, c.y - 10 * c.scale, 35 * c.scale, 0, Math.PI * 2); ctx.arc(c.x + 40 * c.scale, c.y, 30 * c.scale, 0, Math.PI * 2); ctx.fill(); }); // Pipes P.pipes.forEach(pipe => { if (pipe.broken) ctx.globalAlpha = 0.5; // Top Pillar const gradTop = ctx.createLinearGradient(pipe.x, 0, pipe.x + PIPE_WIDTH, 0); gradTop.addColorStop(0, '#475569'); gradTop.addColorStop(0.5, '#64748b'); gradTop.addColorStop(1, '#475569'); ctx.fillStyle = gradTop; ctx.fillRect(pipe.x, 0, PIPE_WIDTH, pipe.topHeight); // Cap Top ctx.fillStyle = '#334155'; ctx.fillRect(pipe.x - 4, pipe.topHeight - 20, PIPE_WIDTH + 8, 20); // Bottom Pillar ctx.fillStyle = gradTop; ctx.fillRect(pipe.x, pipe.topHeight + PIPE_GAP, PIPE_WIDTH, HEIGHT - (pipe.topHeight + PIPE_GAP)); // Cap Bottom ctx.fillStyle = '#334155'; ctx.fillRect(pipe.x - 4, pipe.topHeight + PIPE_GAP, PIPE_WIDTH + 8, 20); ctx.globalAlpha = 1.0; }); // Ground ctx.fillStyle = '#166534'; ctx.fillRect(0, HEIGHT - 40, WIDTH, 40); ctx.fillStyle = '#86efac'; ctx.fillRect(0, HEIGHT - 40, WIDTH, 5); // Moving Ground Pattern ctx.fillStyle = '#14532d'; const groundOffset = (Date.now() / 5 * (P.speed/3.5)) % 40; for(let i = -1; i < WIDTH/20; i++) { ctx.beginPath(); ctx.moveTo(i * 40 - groundOffset, HEIGHT - 35); ctx.lineTo(i * 40 + 20 - groundOffset, HEIGHT); ctx.lineTo(i * 40 + 10 - groundOffset, HEIGHT); ctx.lineTo(i * 40 - 10 - groundOffset, HEIGHT - 35); ctx.fill(); } // Particles P.particles.forEach((p, i) => { p.x += p.vx; p.y += p.vy; p.life -= 0.05; ctx.fillStyle = p.color; ctx.globalAlpha = Math.max(0, p.life); ctx.beginPath(); ctx.arc(p.x, p.y, 3, 0, Math.PI * 2); ctx.fill(); if (p.life <= 0) P.particles.splice(i, 1); }); ctx.globalAlpha = 1.0; // Bird ctx.save(); ctx.translate(100, P.birdY); ctx.rotate(P.rotation); // Boost Aura if (P.invincibleTimer > 0) { ctx.shadowBlur = 20; ctx.shadowColor = '#facc15'; ctx.fillStyle = 'rgba(250, 204, 21, 0.5)'; ctx.beginPath(); ctx.arc(0, 0, 30, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0; } // Tail ctx.fillStyle = '#b91c1c'; ctx.beginPath(); ctx.ellipse(-15, 0, 15, 8, 0, 0, Math.PI*2); ctx.fill(); // Body const bodyGrad = ctx.createRadialGradient(-5, -5, 2, 0, 0, 15); bodyGrad.addColorStop(0, '#0ea5e9'); bodyGrad.addColorStop(1, '#1e3a8a'); ctx.fillStyle = bodyGrad; ctx.beginPath(); ctx.ellipse(0, 0, 18, 14, 0, 0, Math.PI*2); ctx.fill(); // Wing ctx.fillStyle = '#0f766e'; ctx.beginPath(); ctx.ellipse(2, 4, 10, 6, 0.2, 0, Math.PI*2); ctx.fill(); // Eye ctx.fillStyle = 'white'; ctx.beginPath(); ctx.arc(10, -6, 5, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = 'black'; ctx.beginPath(); ctx.arc(12, -6, 2, 0, Math.PI*2); ctx.fill(); // Beak ctx.fillStyle = '#fbbf24'; ctx.beginPath(); ctx.moveTo(14, -4); ctx.lineTo(24, 0); ctx.lineTo(14, 4); ctx.fill(); ctx.restore(); // Flash Effect if (P.flashOpacity > 0) { ctx.fillStyle = `rgba(255, 255, 255, ${P.flashOpacity})`; ctx.fillRect(0, 0, WIDTH, HEIGHT); } requestRef.current = requestAnimationFrame(loop); }; return (
{ if (e.code === 'Space') flap(); }} tabIndex={0}>
{/* Game Container */}
{/* HUD - Score & Level */} {gameState !== 'READY' && (
{score} Level {level}
)} {/* Boost Indicator */} {gameState === 'PLAYING' && boostUnlocked && (
= PIPES_FOR_BOOST ? 'bg-yellow-500 border-white animate-pulse' : 'bg-gray-800 border-gray-600'}`} > {/* Charge Fill */} {!isBoosting && (
)}
{isBoosting ? : = PIPES_FOR_BOOST ? "text-white" : "text-gray-400"}/>}
{boostCharge >= PIPES_FOR_BOOST ? 'Press E' : `${boostCharge}/${PIPES_FOR_BOOST}`}
)} {/* Ready Screen */} {gameState === 'READY' && (

Get Ready

Tap / Space to Fly

Reach Lvl 4 to Unlock Boost [E]

)} {/* Game Over Screen */} {gameState === 'GAMEOVER' && (

Game Over

Score

{score}

Level

{level}

{score >= 10 && (
)}

Best Score: {highScore}

)}
); };