import React, { useState, useEffect, useRef } from 'react'; const PlatformerGame = () => { // Canvas dimensions const [canvasWidth, setCanvasWidth] = useState(800); const [canvasHeight, setCanvasHeight] = useState(500); // Game state const [gameStarted, setGameStarted] = useState(false); const [deaths, setDeaths] = useState(0); const [level, setLevel] = useState(1); const [showTutorial, setShowTutorial] = useState(true); const [isFullscreen, setIsFullscreen] = useState(false); // Refs const canvasRef = useRef(null); const gameContainerRef = useRef(null); const requestIdRef = useRef(null); // Game objects const [player, setPlayer] = useState({ x: 50, y: 400, width: 30, height: 50, velocityX: 0, velocityY: 0, isJumping: false, abilities: { doubleJump: false, wallJump: false, dash: false, } }); // Memory echoes from previous deaths const [memories, setMemories] = useState([]); // Platforms for the current level const [platforms, setPlatforms] = useState([]); // Hazards for the current level const [hazards, setHazards] = useState([]); // Goal for the current level const [goal, setGoal] = useState({ x: 750, y: 400, width: 30, height: 50 }); // Controls state const [keys, setKeys] = useState({ left: false, right: false, up: false, down: false, }); // Scale factor for fullscreen mode const [scale, setScale] = useState(1); // Toggle fullscreen mode const toggleFullscreen = () => { if (!document.fullscreenElement) { // Enter fullscreen if (gameContainerRef.current.requestFullscreen) { gameContainerRef.current.requestFullscreen() .then(() => { setIsFullscreen(true); adjustCanvasSize(); }) .catch(err => { console.error(`Error attempting to enable fullscreen: ${err.message}`); }); } else if (gameContainerRef.current.webkitRequestFullscreen) { gameContainerRef.current.webkitRequestFullscreen(); setIsFullscreen(true); adjustCanvasSize(); } else if (gameContainerRef.current.mozRequestFullScreen) { gameContainerRef.current.mozRequestFullScreen(); setIsFullscreen(true); adjustCanvasSize(); } else if (gameContainerRef.current.msRequestFullscreen) { gameContainerRef.current.msRequestFullscreen(); setIsFullscreen(true); adjustCanvasSize(); } } else { // Exit fullscreen if (document.exitFullscreen) { document.exitFullscreen() .then(() => { setIsFullscreen(false); // Reset to default size setCanvasWidth(800); setCanvasHeight(500); setScale(1); }) .catch(err => { console.error(`Error attempting to exit fullscreen: ${err.message}`); }); } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); setIsFullscreen(false); setCanvasWidth(800); setCanvasHeight(500); setScale(1); } else if (document.mozCancelFullScreen) { document.mozCancelFullScreen(); setIsFullscreen(false); setCanvasWidth(800); setCanvasHeight(500); setScale(1); } else if (document.msExitFullscreen) { document.msExitFullscreen(); setIsFullscreen(false); setCanvasWidth(800); setCanvasHeight(500); setScale(1); } } }; // Adjust canvas size when entering/exiting fullscreen const adjustCanvasSize = () => { if (isFullscreen) { const newWidth = window.innerWidth; const newHeight = window.innerHeight; setCanvasWidth(newWidth); setCanvasHeight(newHeight); // Calculate scale factor to maintain game proportions const widthScale = newWidth / 800; const heightScale = newHeight / 500; setScale(Math.min(widthScale, heightScale)); } else { setCanvasWidth(800); setCanvasHeight(500); setScale(1); } }; // Listen for fullscreen change events useEffect(() => { const handleFullscreenChange = () => { setIsFullscreen(!!document.fullscreenElement); adjustCanvasSize(); }; document.addEventListener('fullscreenchange', handleFullscreenChange); document.addEventListener('webkitfullscreenchange', handleFullscreenChange); document.addEventListener('mozfullscreenchange', handleFullscreenChange); document.addEventListener('MSFullscreenChange', handleFullscreenChange); return () => { document.removeEventListener('fullscreenchange', handleFullscreenChange); document.removeEventListener('webkitfullscreenchange', handleFullscreenChange); document.removeEventListener('mozfullscreenchange', handleFullscreenChange); document.removeEventListener('MSFullscreenChange', handleFullscreenChange); }; }, []); // Handle window resize in fullscreen mode useEffect(() => { const handleResize = () => { if (isFullscreen) { adjustCanvasSize(); } }; window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); }; }, [isFullscreen]); // Initialize level useEffect(() => { if (gameStarted) { initLevel(level); } }, [gameStarted, level]); // Set up keyboard listeners useEffect(() => { const handleKeyDown = (e) => { if (!gameStarted && e.code === 'Space') { setGameStarted(true); setShowTutorial(false); return; } if (e.code === 'ArrowLeft' || e.code === 'KeyA') { setKeys(prev => ({ ...prev, left: true })); } if (e.code === 'ArrowRight' || e.code === 'KeyD') { setKeys(prev => ({ ...prev, right: true })); } if ((e.code === 'ArrowUp' || e.code === 'KeyW' || e.code === 'Space') && !player.isJumping) { setKeys(prev => ({ ...prev, up: true })); setPlayer(prev => ({ ...prev, isJumping: true, velocityY: -15 })); } // Fullscreen toggle with F key if (e.code === 'KeyF') { toggleFullscreen(); } }; const handleKeyUp = (e) => { if (e.code === 'ArrowLeft' || e.code === 'KeyA') { setKeys(prev => ({ ...prev, left: false })); } if (e.code === 'ArrowRight' || e.code === 'KeyD') { setKeys(prev => ({ ...prev, right: false })); } if (e.code === 'ArrowUp' || e.code === 'KeyW' || e.code === 'Space') { setKeys(prev => ({ ...prev, up: false })); } }; window.addEventListener('keydown', handleKeyDown); window.addEventListener('keyup', handleKeyUp); return () => { window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keyup', handleKeyUp); }; }, [gameStarted, player.isJumping]); // Game loop useEffect(() => { if (!gameStarted) return; const render = () => { const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); // Clear canvas ctx.fillStyle = '#3c1874'; // Deep indigo background ctx.fillRect(0, 0, canvasWidth, canvasHeight); // Save the current transformation matrix ctx.save(); // Scale everything based on fullscreen scale factor if (isFullscreen) { // Calculate the centered position const offsetX = (canvasWidth - (800 * scale)) / 2; const offsetY = (canvasHeight - (500 * scale)) / 2; ctx.translate(offsetX, offsetY); ctx.scale(scale, scale); } // Draw platforms ctx.fillStyle = '#20b2aa'; // Teal platforms platforms.forEach(platform => { ctx.fillRect(platform.x, platform.y, platform.width, platform.height); }); // Draw hazards ctx.fillStyle = '#ff4757'; // Red hazards hazards.forEach(hazard => { ctx.fillRect(hazard.x, hazard.y, hazard.width, hazard.height); }); // Draw memories ctx.fillStyle = '#ffbf00'; // Amber memories memories.forEach(memory => { ctx.beginPath(); ctx.arc(memory.x, memory.y, 10, 0, Math.PI * 2); ctx.fill(); // Add glow effect ctx.shadowBlur = 15; ctx.shadowColor = '#ffbf00'; ctx.fill(); ctx.shadowBlur = 0; }); // Draw goal ctx.fillStyle = '#2ecc71'; // Green goal ctx.fillRect(goal.x, goal.y, goal.width, goal.height); // Draw player ctx.fillStyle = '#ff7f50'; // Coral player ctx.fillRect(player.x, player.y, player.width, player.height); // Restore the transformation matrix ctx.restore(); // Draw HUD (not scaled, always in the corner) ctx.fillStyle = 'white'; ctx.font = '20px Inter, sans-serif'; ctx.fillText(`Deaths: ${deaths}`, 20, 30); ctx.fillText(`Level: ${level}`, 20, 60); // Draw abilities ctx.fillText('Abilities:', canvasWidth - 150, 30); ctx.fillText(`Double Jump: ${player.abilities.doubleJump ? '✓' : '✗'}`, canvasWidth - 150, 60); ctx.fillText(`Wall Jump: ${player.abilities.wallJump ? '✓' : '✗'}`, canvasWidth - 150, 90); ctx.fillText(`Dash: ${player.abilities.dash ? '✓' : '✗'}`, canvasWidth - 150, 120); // Draw fullscreen indicator ctx.fillText(`Press F for ${isFullscreen ? 'windowed' : 'fullscreen'}`, canvasWidth - 250, canvasHeight - 20); }; const update = () => { // Update player position based on velocity let newPlayer = { ...player }; // Apply gravity newPlayer.velocityY += 0.8; // Apply horizontal movement if (keys.left) { newPlayer.velocityX = -5; } else if (keys.right) { newPlayer.velocityX = 5; } else { newPlayer.velocityX = 0; } // Update position newPlayer.x += newPlayer.velocityX; newPlayer.y += newPlayer.velocityY; // Check platform collisions let onGround = false; platforms.forEach(platform => { if ( newPlayer.x < platform.x + platform.width && newPlayer.x + newPlayer.width > platform.x && newPlayer.y + newPlayer.height > platform.y && newPlayer.y < platform.y + platform.height ) { // Collision detected // Check if landing on top of platform if ( player.y + player.height <= platform.y && newPlayer.y + newPlayer.height >= platform.y ) { newPlayer.y = platform.y - newPlayer.height; newPlayer.velocityY = 0; newPlayer.isJumping = false; onGround = true; } // Check if hitting bottom of platform else if ( player.y >= platform.y + platform.height && newPlayer.y <= platform.y + platform.height ) { newPlayer.y = platform.y + platform.height; newPlayer.velocityY = 0; } // Check if hitting left side of platform else if ( player.x + player.width <= platform.x && newPlayer.x + newPlayer.width >= platform.x ) { newPlayer.x = platform.x - newPlayer.width; newPlayer.velocityX = 0; } // Check if hitting right side of platform else if ( player.x >= platform.x + platform.width && newPlayer.x <= platform.x + platform.width ) { newPlayer.x = platform.x + platform.width; newPlayer.velocityX = 0; } } }); if (!onGround) { newPlayer.isJumping = true; } // Check boundaries if (newPlayer.x < 0) { newPlayer.x = 0; } if (newPlayer.x + newPlayer.width > 800) { // Keep original game width boundary newPlayer.x = 800 - newPlayer.width; } // Check for death (falling off screen or hitting hazards) if (newPlayer.y > 500) { // Keep original game height boundary handleDeath(newPlayer.x, 490); return; } // Check hazard collisions for (const hazard of hazards) { if ( newPlayer.x < hazard.x + hazard.width && newPlayer.x + newPlayer.width > hazard.x && newPlayer.y < hazard.y + hazard.height && newPlayer.y + newPlayer.height > hazard.y ) { handleDeath(newPlayer.x, newPlayer.y); return; } } // Check memory collisions const newMemories = [...memories]; for (let i = newMemories.length - 1; i >= 0; i--) { const memory = newMemories[i]; const dx = (newPlayer.x + newPlayer.width / 2) - memory.x; const dy = (newPlayer.y + newPlayer.height / 2) - memory.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < 30) { // If player is close enough to memory // Grant ability based on memory type if (memory.type === 'doubleJump') { newPlayer.abilities.doubleJump = true; } else if (memory.type === 'wallJump') { newPlayer.abilities.wallJump = true; } else if (memory.type === 'dash') { newPlayer.abilities.dash = true; } // Remove collected memory newMemories.splice(i, 1); } } // Check goal collision if ( newPlayer.x < goal.x + goal.width && newPlayer.x + newPlayer.width > goal.x && newPlayer.y < goal.y + goal.height && newPlayer.y + newPlayer.height > goal.y ) { // Level completed setLevel(prev => prev + 1); return; } setPlayer(newPlayer); setMemories(newMemories); }; const gameLoop = () => { update(); render(); requestIdRef.current = requestAnimationFrame(gameLoop); }; requestIdRef.current = requestAnimationFrame(gameLoop); return () => { cancelAnimationFrame(requestIdRef.current); }; }, [gameStarted, player, platforms, hazards, memories, keys, goal, deaths, level, canvasWidth, canvasHeight, isFullscreen, scale]); // Handle player death const handleDeath = (x, y) => { setDeaths(prev => prev + 1); // Create a new memory at death location const newMemory = { x, y, type: determineMemoryType(deaths) }; setMemories(prev => [...prev, newMemory]); // Reset player position setPlayer(prev => ({ ...prev, x: 50, y: 400, velocityX: 0, velocityY: 0, isJumping: false })); }; // Determine what type of ability the memory should grant const determineMemoryType = (deathCount) => { if (deathCount % 3 === 0) return 'doubleJump'; if (deathCount % 3 === 1) return 'wallJump'; return 'dash'; }; // Initialize level with platforms and hazards const initLevel = (levelNum) => { let newPlatforms = []; let newHazards = []; let newGoal = { x: 750, y: 400, width: 30, height: 50 }; switch (levelNum) { case 1: // Basic level with a few platforms and a gap newPlatforms = [ { x: 0, y: 450, width: 300, height: 50 }, { x: 400, y: 450, width: 400, height: 50 }, { x: 500, y: 350, width: 100, height: 20 }, ]; newHazards = [ { x: 300, y: 480, width: 100, height: 20 }, ]; newGoal = { x: 750, y: 400, width: 30, height: 50 }; break; case 2: // More complex level with higher platforms newPlatforms = [ { x: 0, y: 450, width: 200, height: 50 }, { x: 300, y: 400, width: 100, height: 20 }, { x: 500, y: 350, width: 100, height: 20 }, { x: 650, y: 300, width: 150, height: 20 }, ]; newHazards = [ { x: 200, y: 480, width: 100, height: 20 }, { x: 400, y: 480, width: 100, height: 20 }, { x: 600, y: 480, width: 100, height: 20 }, ]; newGoal = { x: 750, y: 250, width: 30, height: 50 }; break; case 3: // Advanced level requiring all abilities newPlatforms = [ { x: 0, y: 450, width: 100, height: 50 }, { x: 200, y: 400, width: 50, height: 20 }, { x: 350, y: 350, width: 50, height: 20 }, { x: 500, y: 300, width: 50, height: 20 }, { x: 650, y: 250, width: 150, height: 20 }, { x: 400, y: 150, width: 20, height: 100 }, // Wall for wall jump ]; newHazards = [ { x: 100, y: 480, width: 700, height: 20 }, { x: 300, y: 350, width: 50, height: 20 }, { x: 450, y: 300, width: 50, height: 20 }, { x: 600, y: 250, width: 50, height: 20 }, ]; newGoal = { x: 750, y: 200, width: 30, height: 50 }; break; default: // Victory level newPlatforms = [ { x: 0, y: 450, width: 800, height: 50 }, ]; newHazards = []; newGoal = { x: 400, y: 400, width: 30, height: 50 }; break; } setPlatforms(newPlatforms); setHazards(newHazards); setGoal(newGoal); setMemories([]); // Clear memories for new level // Reset player position setPlayer(prev => ({ ...prev, x: 50, y: 400, velocityX: 0, velocityY: 0, isJumping: false })); }; return (
Learn from your mistakes to progress further!
You've completed all levels with {deaths} deaths!