import React, { useState, useEffect, useRef, useCallback } from 'react';
import { ObstacleType } from '../types';
import Plane from './Plane';
import Obstacle from './Obstacle';
import {
GAME_WIDTH,
GAME_HEIGHT,
GRAVITY,
LIFT,
PLANE_WIDTH,
PLANE_HEIGHT,
OBSTACLE_WIDTH,
OBSTACLE_GAP,
OBSTACLE_SPEED,
OBSTACLE_INTERVAL,
HORIZONTAL_SPEED,
DIVE_FORCE,
MAX_ROTATION,
ROTATION_VELOCITY_SCALAR,
} from '../constants';
interface GameProps {
onGameOver: (score: number) => void;
}
const Game: React.FC = ({ onGameOver }) => {
const [planeY, setPlaneY] = useState(GAME_HEIGHT / 2);
const [planeX, setPlaneX] = useState(GAME_WIDTH / 2 - PLANE_WIDTH / 2);
const [planeVelocity, setPlaneVelocity] = useState(0);
const [planeRotation, setPlaneRotation] = useState(0);
const [obstacles, setObstacles] = useState([]);
const [score, setScore] = useState(0);
const [keysPressed, setKeysPressed] = useState>(new Set());
const gameLoopRef = useRef();
const obstacleTimerRef = useRef();
const gameContainerRef = useRef(null);
const audioContextRef = useRef(null);
const playSound = useCallback((type: 'jump' | 'score' | 'crash') => {
// FIX: Refactored audio context handling to be clearer and avoid potential linter confusion.
// The original complex logic was likely causing the reported errors.
if (!audioContextRef.current) {
return;
}
if (audioContextRef.current.state === 'suspended') {
audioContextRef.current.resume();
}
const ctx = audioContextRef.current;
const oscillator = ctx.createOscillator();
const gainNode = ctx.createGain();
gainNode.connect(ctx.destination);
oscillator.connect(gainNode);
switch (type) {
case 'jump':
oscillator.type = 'triangle';
oscillator.frequency.setValueAtTime(600, ctx.currentTime);
gainNode.gain.setValueAtTime(0.08, ctx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.00001, ctx.currentTime + 0.2);
oscillator.start(ctx.currentTime);
oscillator.stop(ctx.currentTime + 0.2);
break;
case 'score':
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(880, ctx.currentTime);
gainNode.gain.setValueAtTime(0.1, ctx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.00001, ctx.currentTime + 0.15);
oscillator.start(ctx.currentTime);
oscillator.stop(ctx.currentTime + 0.15);
break;
case 'crash':
oscillator.type = 'sawtooth';
oscillator.frequency.setValueAtTime(220, ctx.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(50, ctx.currentTime + 0.5);
gainNode.gain.setValueAtTime(0.15, ctx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.00001, ctx.currentTime + 0.5);
oscillator.start(ctx.currentTime);
oscillator.stop(ctx.currentTime + 0.5);
break;
}
}, []);
const jump = useCallback(() => {
if (!audioContextRef.current) {
audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();
}
playSound('jump');
setPlaneVelocity(LIFT);
}, [playSound]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === ' ' || e.key === 'ArrowUp') {
e.preventDefault();
jump();
} else {
setKeysPressed(prev => new Set(prev).add(e.key));
}
};
const handleKeyUp = (e: KeyboardEvent) => {
setKeysPressed(prev => {
const newKeys = new Set(prev);
newKeys.delete(e.key);
return newKeys;
});
};
const handlePointerDown = (e: Event) => {
e.preventDefault();
jump();
};
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
window.addEventListener('mousedown', handlePointerDown);
window.addEventListener('touchstart', handlePointerDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
window.removeEventListener('mousedown', handlePointerDown);
window.removeEventListener('touchstart', handlePointerDown);
};
}, [jump]);
const runGameLoop = useCallback(() => {
// --- Physics and Movement ---
let newVelocity = planeVelocity + GRAVITY;
if (keysPressed.has('ArrowDown')) newVelocity += DIVE_FORCE;
let newPlaneY = planeY + newVelocity;
newPlaneY = Math.max(0, Math.min(GAME_HEIGHT - PLANE_HEIGHT, newPlaneY));
let newPlaneX = planeX;
if (keysPressed.has('ArrowLeft')) newPlaneX -= HORIZONTAL_SPEED;
if (keysPressed.has('ArrowRight')) newPlaneX += HORIZONTAL_SPEED;
newPlaneX = Math.max(0, Math.min(GAME_WIDTH - PLANE_WIDTH, newPlaneX));
const newRotation = Math.min(MAX_ROTATION, Math.max(-MAX_ROTATION, newVelocity * ROTATION_VELOCITY_SCALAR));
// --- Obstacles and Score ---
let newScore = score;
const updatedObstacles = obstacles
.map(obstacle => ({ ...obstacle, pos: { ...obstacle.pos, x: obstacle.pos.x - OBSTACLE_SPEED } }))
.filter(obstacle => obstacle.pos.x > -OBSTACLE_WIDTH);
updatedObstacles.forEach(obstacle => {
if (!obstacle.passed && obstacle.pos.x + OBSTACLE_WIDTH < newPlaneX) {
obstacle.passed = true;
newScore += 1;
playSound('score');
}
});
// --- Collision Detection ---
const planeRect = { left: newPlaneX, right: newPlaneX + PLANE_WIDTH, top: newPlaneY, bottom: newPlaneY + PLANE_HEIGHT };
if (planeRect.bottom >= GAME_HEIGHT) {
playSound('crash');
onGameOver(newScore);
return;
}
for (const obstacle of updatedObstacles) {
const obstacleLeft = obstacle.pos.x;
const obstacleRight = obstacle.pos.x + obstacle.width;
const topObstacleBottom = obstacle.pos.y + obstacle.height;
const bottomObstacleTop = obstacle.pos.y + obstacle.height + OBSTACLE_GAP;
const collidesWithTop = planeRect.right > obstacleLeft && planeRect.left < obstacleRight && planeRect.top < topObstacleBottom;
const collidesWithBottom = planeRect.right > obstacleLeft && planeRect.left < obstacleRight && planeRect.bottom > bottomObstacleTop;
if (collidesWithTop || collidesWithBottom) {
playSound('crash');
onGameOver(newScore);
return;
}
}
// --- State Updates ---
setPlaneVelocity(newVelocity);
setPlaneY(newPlaneY);
setPlaneX(newPlaneX);
setPlaneRotation(newRotation);
setObstacles(updatedObstacles);
if(newScore !== score) setScore(newScore);
gameLoopRef.current = requestAnimationFrame(runGameLoop);
}, [planeY, planeX, planeVelocity, obstacles, score, keysPressed, onGameOver, playSound]);
useEffect(() => {
gameLoopRef.current = requestAnimationFrame(runGameLoop);
return () => {
if (gameLoopRef.current) {
cancelAnimationFrame(gameLoopRef.current);
}
};
}, [runGameLoop]);
useEffect(() => {
const generateObstacle = () => {
const topHeight = Math.random() * (GAME_HEIGHT - OBSTACLE_GAP - 100) + 50;
setObstacles(prev => [
...prev,
{
pos: { x: GAME_WIDTH, y: 0 },
width: OBSTACLE_WIDTH,
height: topHeight,
passed: false,
}
]);
};
generateObstacle();
obstacleTimerRef.current = window.setInterval(generateObstacle, OBSTACLE_INTERVAL);
return () => {
if (obstacleTimerRef.current) {
clearInterval(obstacleTimerRef.current);
}
};
}, []);
return (
{obstacles.map((obstacle, index) => (
))}
);
};
export default Game;
{score}
Comments
Post a Comment