Manager
View Site
Name
Type
React (.jsx)
HTML (.html)
Icon
Description
Code Editor
Revision History (0)
import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Gamepad2, Keyboard, Trophy, Play, RefreshCw, Zap, AlertCircle } from 'lucide-react'; /** * NEON VELOCITY * A React + HTML5 Canvas Space Shooter with native Gamepad Support. * * Architecture: * - Uses `useRef` for all high-frequency game logic (60fps loop) to avoid React re-renders. * - Uses React State only for UI overlays (Menus, Score, HUD). * - Implements a custom Input Manager that polls the Gamepad API. */ // --- Constants --- const CANVAS_WIDTH = 1200; const CANVAS_HEIGHT = 800; const PLAYER_SPEED = 7; // Increased from 6 const PLAYER_SIZE = 20; const BULLET_SPEED = 12; const ENEMY_SPEED_BASE = 1.2; // Decreased from 2 (Easier) const SPAWN_RATE_INITIAL = 80; // Increased from 60 (Slower spawns = Easier) const DASH_COOLDOWN = 100; // Decreased from 120 (More frequent dashes) const DASH_DURATION = 15; // Frames // Colors const COLOR_BG = '#09090b'; // Zinc 950 const COLOR_PLAYER = '#3b82f6'; // Blue 500 const COLOR_PLAYER_DASH = '#60a5fa'; // Blue 400 const COLOR_ENEMY = '#ef4444'; // Red 500 const COLOR_BULLET = '#fbbf24'; // Amber 400 const COLOR_PARTICLE = '#f472b6'; // Pink 400 export default function NeonVelocity() { // --- React State for UI --- const [gameState, setGameState] = useState('start'); // 'start', 'playing', 'gameover' const [score, setScore] = useState(0); const [highScore, setHighScore] = useState(0); const [inputType, setInputType] = useState('keyboard'); // 'keyboard' | 'gamepad' const [dashCooldownUI, setDashCooldownUI] = useState(0); // 0-100 percentage for UI // --- Mutable Game State (Refs) --- const canvasRef = useRef(null); const requestRef = useRef(null); const frameCountRef = useRef(0); // Game Entities Container const gameRef = useRef({ player: { x: CANVAS_WIDTH / 2, y: CANVAS_HEIGHT / 2, vx: 0, vy: 0, angle: 0, dashTimer: 0, dashCooldown: 0, hp: 3 }, bullets: [], enemies: [], particles: [], score: 0, spawnRate: SPAWN_RATE_INITIAL, difficultyMultiplier: 1, camera: { x: 0, y: 0, shake: 0 }, keys: {}, // Keyboard state menuLockout: 0, // Debounce for menu actions }); // --- Input Handling --- // Keyboard Listeners useEffect(() => { const handleKeyDown = (e) => { gameRef.current.keys[e.code] = true; setInputType('keyboard'); }; const handleKeyUp = (e) => { gameRef.current.keys[e.code] = false; }; window.addEventListener('keydown', handleKeyDown); window.addEventListener('keyup', handleKeyUp); // Gamepad Connection Events const handleGamepadConnect = () => { setInputType('gamepad'); console.log("Gamepad connected"); }; const handleGamepadDisconnect = () => { setInputType('keyboard'); console.log("Gamepad disconnected"); }; window.addEventListener('gamepadconnected', handleGamepadConnect); window.addEventListener('gamepaddisconnected', handleGamepadDisconnect); return () => { window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keyup', handleKeyUp); window.removeEventListener('gamepadconnected', handleGamepadConnect); window.removeEventListener('gamepaddisconnected', handleGamepadDisconnect); }; }, []); // --- Game Loop Helpers --- const spawnEnemy = () => { const state = gameRef.current; // Spawn at random edge const edge = Math.floor(Math.random() * 4); // 0: top, 1: right, 2: bottom, 3: left let ex, ey; switch(edge) { case 0: ex = Math.random() * CANVAS_WIDTH; ey = -50; break; case 1: ex = CANVAS_WIDTH + 50; ey = Math.random() * CANVAS_HEIGHT; break; case 2: ex = Math.random() * CANVAS_WIDTH; ey = CANVAS_HEIGHT + 50; break; case 3: ex = -50; ey = Math.random() * CANVAS_HEIGHT; break; default: ex = 0; ey = 0; } state.enemies.push({ x: ex, y: ey, size: 20 + Math.random() * 10, hp: 1 + Math.floor(state.score / 500), speed: (ENEMY_SPEED_BASE + (state.score / 2000)) * (0.8 + Math.random() * 0.4), angle: 0 }); }; const createParticles = (x, y, count, color) => { for (let i = 0; i < count; i++) { gameRef.current.particles.push({ x, y, vx: (Math.random() - 0.5) * 10, vy: (Math.random() - 0.5) * 10, life: 1.0, decay: 0.02 + Math.random() * 0.03, color: color || COLOR_PARTICLE }); } }; // --- Main Update Logic --- const update = () => { const state = gameRef.current; const player = state.player; // 1. Input Processing let dx = 0; let dy = 0; let shoot = false; let dash = false; // Gamepad Polling const gamepads = navigator.getGamepads ? navigator.getGamepads() : []; const gp = gamepads[0]; // Assume player 1 if (gp) { if (inputType !== 'gamepad') setInputType('gamepad'); // Deadzone helper const applyDeadzone = (v) => Math.abs(v) > 0.1 ? v : 0; dx = applyDeadzone(gp.axes[0]); dy = applyDeadzone(gp.axes[1]); // Button 0 is A/Cross, Button 5 is R1, Button 7 is R2 if (gp.buttons[0].pressed || gp.buttons[5].pressed || gp.buttons[7].pressed) { shoot = true; } // Button 1 is B/Circle, Button 2 is X/Square if (gp.buttons[1].pressed || gp.buttons[2].pressed) { dash = true; } } else { // Keyboard Fallback if (state.keys['ArrowUp'] || state.keys['KeyW']) dy = -1; if (state.keys['ArrowDown'] || state.keys['KeyS']) dy = 1; if (state.keys['ArrowLeft'] || state.keys['KeyA']) dx = -1; if (state.keys['ArrowRight'] || state.keys['KeyD']) dx = 1; if (state.keys['Space'] || state.keys['KeyZ']) shoot = true; if (state.keys['ShiftLeft'] || state.keys['KeyX']) dash = true; } // Normalize Movement Vector const mag = Math.sqrt(dx * dx + dy * dy); if (mag > 0) { dx /= mag; dy /= mag; // Smooth Rotation toward movement player.angle = Math.atan2(dy, dx); } // 2. Player Logic if (player.dashTimer > 0) { // Dashing state player.dashTimer--; player.x += player.vx; // Keep momentum player.y += player.vy; } else { // Normal state player.vx = dx * PLAYER_SPEED; player.vy = dy * PLAYER_SPEED; player.x += player.vx; player.y += player.vy; // Wall Clamping player.x = Math.max(PLAYER_SIZE, Math.min(CANVAS_WIDTH - PLAYER_SIZE, player.x)); player.y = Math.max(PLAYER_SIZE, Math.min(CANVAS_HEIGHT - PLAYER_SIZE, player.y)); // Dash Activation if (player.dashCooldown > 0) player.dashCooldown--; if (dash && player.dashCooldown <= 0 && mag > 0) { player.dashTimer = DASH_DURATION; player.dashCooldown = DASH_COOLDOWN; player.vx = dx * (PLAYER_SPEED * 3); // Dash burst player.vy = dy * (PLAYER_SPEED * 3); state.camera.shake = 10; createParticles(player.x, player.y, 10, COLOR_PLAYER_DASH); } } // Sync Dash Cooldown to UI (throttled slightly for performance) if (frameCountRef.current % 10 === 0) { const pct = Math.max(0, (player.dashCooldown / DASH_COOLDOWN) * 100); setDashCooldownUI(pct); } // 3. Shooting if (shoot && frameCountRef.current % 8 === 0 && player.dashTimer <= 0) { const angle = player.angle; // Slight spread const spread = (Math.random() - 0.5) * 0.1; state.bullets.push({ x: player.x + Math.cos(angle) * 20, y: player.y + Math.sin(angle) * 20, vx: Math.cos(angle + spread) * BULLET_SPEED, vy: Math.sin(angle + spread) * BULLET_SPEED, life: 60 }); } // 4. Bullets Logic for (let i = state.bullets.length - 1; i >= 0; i--) { const b = state.bullets[i]; b.x += b.vx; b.y += b.vy; b.life--; // Check collision with enemies let hit = false; for (let j = state.enemies.length - 1; j >= 0; j--) { const e = state.enemies[j]; const dist = Math.hypot(b.x - e.x, b.y - e.y); if (dist < e.size + 5) { e.hp--; hit = true; createParticles(e.x, e.y, 3, COLOR_BULLET); if (e.hp <= 0) { state.enemies.splice(j, 1); state.score += 100; state.camera.shake = 5; createParticles(e.x, e.y, 15, COLOR_ENEMY); setScore(state.score); // Sync score to UI } break; // Bullet hits one enemy } } if (hit || b.life <= 0 || b.x < 0 || b.x > CANVAS_WIDTH || b.y < 0 || b.y > CANVAS_HEIGHT) { state.bullets.splice(i, 1); } } // 5. Enemy Logic & Spawning if (frameCountRef.current % Math.floor(state.spawnRate) === 0) { spawnEnemy(); // Increase difficulty slowly if (state.spawnRate > 25) state.spawnRate -= 0.05; } for (let i = state.enemies.length - 1; i >= 0; i--) { const e = state.enemies[i]; // Move towards player const angle = Math.atan2(player.y - e.y, player.x - e.x); e.vx = Math.cos(angle) * e.speed; e.vy = Math.sin(angle) * e.speed; e.x += e.vx; e.y += e.vy; // Player Collision const distToPlayer = Math.hypot(player.x - e.x, player.y - e.y); if (distToPlayer < PLAYER_SIZE + e.size) { // If dashing, kill enemy if (player.dashTimer > 0) { state.enemies.splice(i, 1); state.score += 200; // Bonus for dash kill state.camera.shake = 15; createParticles(e.x, e.y, 20, COLOR_PLAYER_DASH); setScore(state.score); } else { // Game Over state.menuLockout = 60; // 1 second lockout before restart allowed if (state.score > highScore) setHighScore(state.score); setGameState('gameover'); return; // Stop update } } } // 6. Particles for (let i = state.particles.length - 1; i >= 0; i--) { const p = state.particles[i]; p.x += p.vx; p.y += p.vy; p.life -= p.decay; if (p.life <= 0) state.particles.splice(i, 1); } // 7. Camera Shake Decay if (state.camera.shake > 0) state.camera.shake *= 0.9; if (state.camera.shake < 0.5) state.camera.shake = 0; frameCountRef.current++; }; const draw = (ctx) => { const state = gameRef.current; // Clear & Shake ctx.save(); ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); if (state.camera.shake > 0) { const dx = (Math.random() - 0.5) * state.camera.shake; const dy = (Math.random() - 0.5) * state.camera.shake; ctx.translate(dx, dy); } // Draw Grid (Retro effect) ctx.strokeStyle = '#18181b'; // faint grid ctx.lineWidth = 1; for (let x = 0; x <= CANVAS_WIDTH; x += 50) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, CANVAS_HEIGHT); ctx.stroke(); } for (let y = 0; y <= CANVAS_HEIGHT; y += 50) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(CANVAS_WIDTH, y); ctx.stroke(); } // Draw Particles state.particles.forEach(p => { ctx.globalAlpha = p.life; ctx.fillStyle = p.color; ctx.beginPath(); ctx.arc(p.x, p.y, 2 + p.life * 2, 0, Math.PI * 2); ctx.fill(); }); ctx.globalAlpha = 1.0; // Draw Bullets ctx.fillStyle = COLOR_BULLET; state.bullets.forEach(b => { ctx.beginPath(); ctx.arc(b.x, b.y, 4, 0, Math.PI * 2); ctx.fill(); }); // Draw Enemies state.enemies.forEach(e => { ctx.strokeStyle = COLOR_ENEMY; ctx.lineWidth = 2; ctx.shadowColor = COLOR_ENEMY; ctx.shadowBlur = 10; ctx.save(); ctx.translate(e.x, e.y); // Spinning square effect ctx.rotate(frameCountRef.current * 0.05); ctx.strokeRect(-e.size/2, -e.size/2, e.size, e.size); // Inner core ctx.fillStyle = '#7f1d1d'; ctx.fillRect(-e.size/4, -e.size/4, e.size/2, e.size/2); ctx.restore(); }); // Draw Player const p = state.player; ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.angle); ctx.shadowBlur = p.dashTimer > 0 ? 20 : 10; ctx.shadowColor = p.dashTimer > 0 ? COLOR_PLAYER_DASH : COLOR_PLAYER; ctx.fillStyle = p.dashTimer > 0 ? '#fff' : COLOR_BG; ctx.strokeStyle = p.dashTimer > 0 ? COLOR_PLAYER_DASH : COLOR_PLAYER; ctx.lineWidth = 3; // Ship shape (Triangle) ctx.beginPath(); ctx.moveTo(15, 0); ctx.lineTo(-10, 10); ctx.lineTo(-10, -10); ctx.closePath(); ctx.fill(); ctx.stroke(); // Engine flame if (Math.random() > 0.5) { ctx.fillStyle = '#3b82f6'; // Light blue flame ctx.beginPath(); ctx.moveTo(-12, 5); ctx.lineTo(-20 - Math.random() * 10, 0); ctx.lineTo(-12, -5); ctx.fill(); } ctx.restore(); // Reset shadow for next frame ctx.shadowBlur = 0; ctx.restore(); }; const loop = useCallback(() => { // Controller Menu Input Polling if (gameState !== 'playing') { if (gameRef.current.menuLockout > 0) { gameRef.current.menuLockout--; } else { const gamepads = navigator.getGamepads ? navigator.getGamepads() : []; const gp = gamepads[0]; if (gp) { // Button 0 (A/Cross) or 9 (Start) if (gp.buttons[0].pressed || gp.buttons[9]?.pressed) { if (!gameRef.current.keys['MenuPressed']) { // Simple debounce for press event startGame(); gameRef.current.keys['MenuPressed'] = true; } } else { gameRef.current.keys['MenuPressed'] = false; } } } } if (gameState === 'playing') { update(); if (canvasRef.current) { const ctx = canvasRef.current.getContext('2d'); if (ctx) draw(ctx); } } requestRef.current = requestAnimationFrame(loop); }, [gameState]); // Start/Stop Loop useEffect(() => { // Always run loop to poll inputs in menu requestRef.current = requestAnimationFrame(loop); return () => cancelAnimationFrame(requestRef.current); }, [gameState, loop]); // --- Game Management --- const startGame = () => { // Reset state gameRef.current = { player: { x: CANVAS_WIDTH / 2, y: CANVAS_HEIGHT / 2, vx: 0, vy: 0, angle: 0, dashTimer: 0, dashCooldown: 0, hp: 3 }, bullets: [], enemies: [], particles: [], score: 0, spawnRate: SPAWN_RATE_INITIAL, difficultyMultiplier: 1, camera: { x: 0, y: 0, shake: 0 }, keys: {}, menuLockout: 0, }; setScore(0); setGameState('playing'); }; // --- UI Components --- const GameUI = () => ( <div className="absolute top-0 left-0 w-full h-full pointer-events-none p-6 flex flex-col justify-between"> {/* HUD Top */} <div className="flex justify-between items-start"> <div className="bg-zinc-900/80 border border-zinc-800 p-4 rounded-xl backdrop-blur-sm shadow-xl"> <div className="text-zinc-400 text-sm font-bold tracking-widest uppercase mb-1">Score</div> <div className="text-4xl font-black text-white font-mono">{score.toLocaleString()}</div> </div> {/* Dash Indicator */} <div className="bg-zinc-900/80 border border-zinc-800 p-4 rounded-xl backdrop-blur-sm shadow-xl flex flex-col items-center gap-2"> <div className="text-zinc-400 text-sm font-bold tracking-widest uppercase">Dash Charge</div> <div className="w-32 h-2 bg-zinc-800 rounded-full overflow-hidden"> <div className={`h-full transition-all duration-200 ${100 - dashCooldownUI >= 100 ? 'bg-blue-400' : 'bg-zinc-600'}`} style={{ width: `${100 - dashCooldownUI}%` }} /> </div> <div className="flex items-center gap-2 text-xs text-zinc-500"> {inputType === 'gamepad' ? <><div className="px-1.5 py-0.5 bg-zinc-700 rounded border border-zinc-600 text-white font-bold">B</div> or <div className="px-1.5 py-0.5 bg-zinc-700 rounded border border-zinc-600 text-white font-bold">◯</div></> : <div className="px-1.5 py-0.5 bg-zinc-700 rounded border border-zinc-600 text-white font-bold">SHIFT</div> } </div> </div> <div className="bg-zinc-900/80 border border-zinc-800 p-4 rounded-xl backdrop-blur-sm shadow-xl"> <div className="text-zinc-400 text-sm font-bold tracking-widest uppercase mb-1 text-right">Input</div> <div className="flex items-center gap-2 text-zinc-300"> {inputType === 'gamepad' ? <Gamepad2 className="w-5 h-5 text-green-400" /> : <Keyboard className="w-5 h-5" />} <span className="text-sm font-medium">{inputType === 'gamepad' ? 'Controller Active' : 'Keyboard'}</span> </div> </div> </div> </div> ); const StartScreen = () => ( <div className="absolute inset-0 flex items-center justify-center bg-black/80 backdrop-blur-sm z-50"> <div className="bg-zinc-900 border border-zinc-800 p-8 rounded-2xl max-w-lg w-full shadow-2xl text-center"> <h1 className="text-6xl font-black text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-400 mb-2 italic transform -skew-x-12"> NEON VELOCITY </h1> <p className="text-zinc-400 mb-8 text-lg">High-speed survival shooter</p> <div className="grid grid-cols-2 gap-4 mb-8 text-left"> <div className="bg-zinc-950 p-4 rounded-xl border border-zinc-800"> <div className="flex items-center gap-2 mb-3 text-blue-400 font-bold"> <Gamepad2 /> Controller </div> <ul className="text-sm text-zinc-400 space-y-2"> <li className="flex justify-between"><span>Move</span> <span className="text-white">L-Stick</span></li> <li className="flex justify-between"><span>Shoot</span> <span className="text-white">A / X / R1</span></li> <li className="flex justify-between"><span>Dash</span> <span className="text-white">B / O / L1</span></li> </ul> </div> <div className="bg-zinc-950 p-4 rounded-xl border border-zinc-800"> <div className="flex items-center gap-2 mb-3 text-purple-400 font-bold"> <Keyboard /> Keyboard </div> <ul className="text-sm text-zinc-400 space-y-2"> <li className="flex justify-between"><span>Move</span> <span className="text-white">WASD / Arrows</span></li> <li className="flex justify-between"><span>Shoot</span> <span className="text-white">Space</span></li> <li className="flex justify-between"><span>Dash</span> <span className="text-white">Shift</span></li> </ul> </div> </div> {inputType !== 'gamepad' && ( <div className="mb-6 flex items-center justify-center gap-2 text-yellow-500 bg-yellow-500/10 p-2 rounded-lg text-sm"> <AlertCircle className="w-4 h-4" /> <span>Connect a Bluetooth Controller for best experience!</span> </div> )} <button onClick={startGame} className="w-full py-4 bg-blue-600 hover:bg-blue-500 text-white font-bold text-xl rounded-xl transition-all hover:scale-105 active:scale-95 shadow-lg shadow-blue-900/20 flex items-center justify-center gap-2" > <Play fill="currentColor" /> START MISSION {inputType === 'gamepad' && <span className="ml-2 text-sm bg-black/20 px-2 py-0.5 rounded border border-white/20">Press A</span>} </button> </div> </div> ); const GameOverScreen = () => ( <div className="absolute inset-0 flex items-center justify-center bg-red-950/80 backdrop-blur-md z-50"> <div className="bg-zinc-900 border border-red-900/50 p-8 rounded-2xl max-w-md w-full shadow-2xl text-center"> <div className="text-red-500 font-black text-5xl mb-2 tracking-tighter">CRITICAL FAILURE</div> <p className="text-zinc-400 mb-6">System Overrun</p> <div className="bg-black/50 p-6 rounded-xl mb-8 border border-zinc-800"> <div className="text-zinc-500 text-sm font-bold uppercase tracking-widest mb-1">Final Score</div> <div className="text-5xl font-mono text-white mb-4">{score.toLocaleString()}</div> {score >= highScore && score > 0 && ( <div className="flex items-center justify-center gap-2 text-yellow-400 font-bold text-sm bg-yellow-400/10 py-1 px-3 rounded-full mx-auto w-fit"> <Trophy className="w-4 h-4" /> NEW HIGH SCORE </div> )} </div> <button onClick={startGame} className="w-full py-4 bg-white hover:bg-zinc-200 text-black font-bold text-xl rounded-xl transition-all hover:scale-105 active:scale-95 flex items-center justify-center gap-2" > <RefreshCw className="w-5 h-5" /> REBOOT SYSTEM {inputType === 'gamepad' && <span className="ml-2 text-sm bg-black/10 px-2 py-0.5 rounded border border-black/20">Press A</span>} </button> </div> </div> ); return ( <div className="relative w-full h-screen bg-zinc-950 overflow-hidden flex items-center justify-center font-sans select-none"> {/* Container ensures aspect ratio or fitting */} <div className="relative shadow-2xl shadow-blue-900/20 border border-zinc-800 rounded-lg overflow-hidden" style={{ width: 1200, height: 800, maxWidth: '100%', maxHeight: '100%', aspectRatio: '3/2' }}> <canvas ref={canvasRef} width={CANVAS_WIDTH} height={CANVAS_HEIGHT} className="w-full h-full block bg-zinc-950" /> {/* UI Layer */} {gameState === 'start' && <StartScreen />} {gameState === 'playing' && <GameUI />} {gameState === 'gameover' && <GameOverScreen />} {/* Hints Layer */} {gameState === 'playing' && ( <div className="absolute bottom-6 left-1/2 transform -translate-x-1/2 flex gap-4 text-xs text-zinc-600 font-mono uppercase tracking-widest opacity-50"> <span className="flex items-center gap-1"><div className="w-2 h-2 rounded-full bg-blue-500"></div> Player</span> <span className="flex items-center gap-1"><div className="w-2 h-2 border border-red-500"></div> Enemy</span> <span className="flex items-center gap-1"><div className="w-2 h-2 rounded-full bg-yellow-500"></div> Projectile</span> </div> )} </div> </div> ); }
No revisions found. Save the file to create a backup.
Delete
Update App