Merge version_3 into main #4

Merged
bender merged 4 commits from version_3 into main 2026-03-10 14:11:12 +00:00
4 changed files with 872 additions and 70 deletions

View File

@@ -17,6 +17,192 @@ import {
Trophy,
Gamepad2,
} from "lucide-react";
import { useState, useEffect, useRef } from "react";
type GameState = "idle" | "playing" | "paused" | "gameover";
interface GameInstance {
id: string;
state: GameState;
score: number;
level: number;
}
const GameContainer = ({ gameId }: { gameId: string }) => {
const [gameState, setGameState] = useState<GameState>("idle");
const [score, setScore] = useState(0);
const [level, setLevel] = useState(1);
const canvasRef = useRef<HTMLCanvasElement>(null);
const gameLoopRef = useRef<number | null>(null);
const keysPressed = useRef<{ [key: string]: boolean }>({});
// Keyboard event handlers
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
keysPressed.current[e.key.toLowerCase()] = true;
// Space to start/pause/resume
if (e.key === " ") {
e.preventDefault();
if (gameState === "idle" || gameState === "gameover") {
setGameState("playing");
} else if (gameState === "playing") {
setGameState("paused");
} else if (gameState === "paused") {
setGameState("playing");
}
}
};
const handleKeyUp = (e: KeyboardEvent) => {
keysPressed.current[e.key.toLowerCase()] = false;
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
};
}, [gameState]);
// Mouse event handler
const handleCanvasClick = (e: React.MouseEvent<HTMLCanvasElement>) => {
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Simple click-based interaction
if (gameState === "playing") {
setScore((prev) => prev + 10);
}
};
// Game loop
useEffect(() => {
if (gameState !== "playing") return;
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let animationId: number;
const gameLoop = () => {
// Clear canvas
ctx.fillStyle = "rgba(15, 23, 42, 0.8)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw game elements
ctx.fillStyle = "#00ff88";
ctx.font = "24px Arial";
ctx.fillText(`Score: ${score}`, 20, 40);
ctx.fillText(`Level: ${level}`, 20, 80);
// Draw controls hint
if (gameState === "playing") {
ctx.fillStyle = "rgba(255, 255, 255, 0.5)";
ctx.font = "14px Arial";
ctx.fillText("Arrow keys: Move | Space: Pause | Click: Action", 20, canvas.height - 20);
}
// Update score every frame
if (gameState === "playing") {
setScore((prev) => prev + 1);
}
animationId = requestAnimationFrame(gameLoop);
};
animationId = requestAnimationFrame(gameLoop);
return () => cancelAnimationFrame(animationId);
}, [gameState, score, level]);
const handlePauseResume = () => {
if (gameState === "playing") {
setGameState("paused");
} else if (gameState === "paused") {
setGameState("playing");
}
};
const handleGameOver = () => {
setGameState("gameover");
};
const handleReset = () => {
setScore(0);
setLevel(1);
setGameState("idle");
};
return (
<div className="w-full flex flex-col gap-4 p-4 bg-slate-900 rounded-lg">
<canvas
ref={canvasRef}
width={800}
height={400}
onClick={handleCanvasClick}
className="w-full border-2 border-green-400 bg-slate-800 rounded cursor-pointer"
/>
<div className="flex gap-2 justify-center flex-wrap">
{gameState === "idle" && (
<button
onClick={() => setGameState("playing")}
className="px-6 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition"
>
Start Game
</button>
)}
{gameState === "playing" && (
<button
onClick={handlePauseResume}
className="px-6 py-2 bg-yellow-500 text-white rounded hover:bg-yellow-600 transition"
>
Pause (Space)
</button>
)}
{gameState === "paused" && (
<button
onClick={handlePauseResume}
className="px-6 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition"
>
Resume (Space)
</button>
)}
{(gameState === "playing" || gameState === "paused") && (
<button
onClick={handleGameOver}
className="px-6 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition"
>
End Game
</button>
)}
{gameState === "gameover" && (
<button
onClick={handleReset}
className="px-6 py-2 bg-purple-500 text-white rounded hover:bg-purple-600 transition"
>
Play Again
</button>
)}
</div>
<div className="text-center text-sm text-gray-400">
{gameState === "idle" && "Press Space or click Start to begin"}
{gameState === "playing" && "Game Running - Press Space to pause"}
{gameState === "paused" && "Game Paused - Press Space to resume"}
{gameState === "gameover" && `Game Over! Final Score: ${score}`}
</div>
</div>
);
};
export default function HomePage() {
const navItems = [
@@ -111,18 +297,15 @@ export default function HomePage() {
{
id: "featured-1", brand: "PlayHub", name: "Neon Blasters Pro", price: "FREE", rating: 5,
reviewCount: "12.5k", imageSrc:
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-vibrant-action-game-thumbnail-with-exp-1773147106304-b4d00265.jpg", imageAlt: "Neon Blasters Pro action game"
},
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-vibrant-action-game-thumbnail-with-exp-1773147106304-b4d00265.jpg", imageAlt: "Neon Blasters Pro action game"},
{
id: "featured-2", brand: "PlayHub", name: "Speed Zone Racing", price: "FREE", rating: 5,
reviewCount: "9.8k", imageSrc:
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-racing-game-screenshot-showing-high-sp-1773147106485-71fc80aa.png", imageAlt: "Speed Zone Racing game"
},
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-racing-game-screenshot-showing-high-sp-1773147106485-71fc80aa.png", imageAlt: "Speed Zone Racing game"},
{
id: "featured-3", brand: "PlayHub", name: "Puzzle Master", price: "FREE", rating: 5,
reviewCount: "15.2k", imageSrc:
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-puzzle-game-interface-with-colorful-ma-1773147105895-ad36efba.jpg", imageAlt: "Puzzle Master game"
},
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-puzzle-game-interface-with-colorful-ma-1773147105895-ad36efba.jpg", imageAlt: "Puzzle Master game"},
]}
gridVariant="uniform-all-items-equal"
animationType="slide-up"
@@ -141,43 +324,37 @@ export default function HomePage() {
icon: Zap,
title: "Action", description:
"High-octane gaming with intense combat and explosive gameplay. Perfect for adrenaline seekers.", button: {
text: "Play Action Games", href: "/games"
},
text: "Play Action Games", href: "/games"},
},
{
icon: Gauge,
title: "Racing", description:
"Speed through tracks with realistic vehicles and competitive racing experiences.", button: {
text: "Play Racing Games", href: "/games"
},
text: "Play Racing Games", href: "/games"},
},
{
icon: Brain,
title: "Puzzle", description:
"Challenge your mind with logic puzzles, match-3 games, and brain teasers.", button: {
text: "Play Puzzle Games", href: "/games"
},
text: "Play Puzzle Games", href: "/games"},
},
{
icon: Compass,
title: "Adventure", description:
"Embark on epic quests and explore immersive worlds filled with mystery and wonder.", button: {
text: "Play Adventure Games", href: "/games"
},
text: "Play Adventure Games", href: "/games"},
},
{
icon: Trophy,
title: "Sports", description:
"Compete in various sports simulations from football to basketball to esports.", button: {
text: "Play Sports Games", href: "/games"
},
text: "Play Sports Games", href: "/games"},
},
{
icon: Gamepad2,
title: "Arcade", description:
"Classic arcade thrills with retro gameplay, pixel art, and nostalgic gaming joy.", button: {
text: "Play Arcade Games", href: "/games"
},
text: "Play Arcade Games", href: "/games"},
},
]}
animationType="slide-up"
@@ -187,38 +364,23 @@ export default function HomePage() {
</div>
<div id="popular-games" data-section="popular-games">
<ProductCardTwo
title="Popular Games This Week"
description="Join millions of players enjoying these trending games right now. Played by over 50 million gamers worldwide."
tag="Trending"
products={[
{
id: "popular-1", brand: "PlayHub Studio", name: "Team Legends Battle", price: "FREE", rating: 4,
reviewCount: "45.3k", imageSrc:
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-popular-multiplayer-game-screenshot-fe-1773147108371-57fdc73e.png", imageAlt: "Team Legends Battle multiplayer game"
},
{
id: "popular-2", brand: "PlayHub Studio", name: "Strategic Conquest", price: "FREE", rating: 5,
reviewCount: "32.1k", imageSrc:
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-strategy-game-interface-showing-game-b-1773147107363-605d5cf4.png", imageAlt: "Strategic Conquest game"
},
{
id: "popular-3", brand: "PlayHub Studio", name: "Precision Fire Elite", price: "FREE", rating: 4,
reviewCount: "38.7k", imageSrc:
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-shooting-game-screenshot-with-first-pe-1773147107276-7261d147.png", imageAlt: "Precision Fire Elite shooter game"
},
{
id: "popular-4", brand: "PlayHub Studio", name: "Zen Garden Match", price: "FREE", rating: 5,
reviewCount: "28.4k", imageSrc:
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-casual-mobile-game-screenshot-with-sim-1773147107159-09cfc4e7.png", imageAlt: "Zen Garden Match casual game"
},
]}
gridVariant="uniform-all-items-equal"
animationType="slide-up"
textboxLayout="default"
useInvertedBackground={false}
carouselMode="auto"
/>
<div className="w-full max-w-7xl mx-auto px-4 py-16">
<h2 className="text-3xl font-bold mb-4">Popular Games This Week - Play Now</h2>
<p className="text-gray-400 mb-8">Experience interactive gameplay with keyboard/mouse controls. Press Space to pause/resume, click to interact.</p>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<GameContainer gameId="game-1" />
<GameContainer gameId="game-2" />
</div>
<div className="mt-8 p-4 bg-slate-800 rounded text-sm text-gray-300">
<p className="font-semibold mb-2">Game Controls:</p>
<ul className="list-disc list-inside space-y-1">
<li><span className="text-green-400">Arrow Keys</span> - Move player/control direction</li>
<li><span className="text-green-400">Space Bar</span> - Start/Pause/Resume game</li>
<li><span className="text-green-400">Mouse Click</span> - Interact with game elements</li>
<li><span className="text-green-400">Play Again</span> - Reset and start over after game over</li>
</ul>
</div>
</div>
</div>
<div id="testimonials" data-section="testimonials">
@@ -230,33 +392,27 @@ export default function HomePage() {
{
id: "test-1", title: "Best free gaming platform ever", quote:
"PlayHub changed my gaming life! I can play so many games without installing anything. The variety and quality are incredible. Highly recommend!", name: "Alex Chen", role: "Gaming Enthusiast", imageSrc:
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-professional-gaming-enthusiast-portrai-1773147108470-29bb2604.png", imageAlt: "Alex Chen testimonial"
},
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-professional-gaming-enthusiast-portrai-1773147108470-29bb2604.png", imageAlt: "Alex Chen testimonial"},
{
id: "test-2", title: "Perfect for casual gaming", quote:
"I love how easy it is to jump into a game on PlayHub. Whether I have 5 minutes or an hour, there's something perfect for me. Best discovery ever!", name: "Sarah Williams", role: "Casual Player", imageSrc:
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-young-casual-gamer-portrait-smiling-ho-1773147107760-3f1647bf.jpg", imageAlt: "Sarah Williams testimonial"
},
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-young-casual-gamer-portrait-smiling-ho-1773147107760-3f1647bf.jpg", imageAlt: "Sarah Williams testimonial"},
{
id: "test-3", title: "Serious gamer's paradise", quote:
"The competitive games on PlayHub are insane! Great balance, smooth gameplay, and an amazing community. This is where I spend most of my gaming time.", name: "Marcus Johnson", role: "Competitive Player", imageSrc:
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-competitive-gamer-portrait-intense-foc-1773147108094-9100416c.jpg", imageAlt: "Marcus Johnson testimonial"
},
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-competitive-gamer-portrait-intense-foc-1773147108094-9100416c.jpg", imageAlt: "Marcus Johnson testimonial"},
{
id: "test-4", title: "Something for everyone", quote:
"My whole family plays different games on PlayHub. From puzzle games to action, everyone finds what they love. It's a game-changer for households!", name: "Emma Rodriguez", role: "Family Gamer", imageSrc:
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-diverse-gamer-portrait-friendly-approa-1773147107832-c70ea8c8.png", imageAlt: "Emma Rodriguez testimonial"
},
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-diverse-gamer-portrait-friendly-approa-1773147107832-c70ea8c8.png", imageAlt: "Emma Rodriguez testimonial"},
{
id: "test-5", title: "Mobile gaming done right", quote:
"I play on my phone during commutes. PlayHub's mobile experience is smooth, responsive, and packed with amazing games. No lags, pure gaming joy!", name: "David Park", role: "Mobile Gamer", imageSrc:
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-mobile-gamer-portrait-casual-comfortab-1773147108245-b78865a8.jpg", imageAlt: "David Park testimonial"
},
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-mobile-gamer-portrait-casual-comfortab-1773147108245-b78865a8.jpg", imageAlt: "David Park testimonial"},
{
id: "test-6", title: "The ultimate gaming hub", quote:
"As a hardcore gamer, I was skeptical, but PlayHub delivers. The game selection is vast, servers are reliable, and the community is fantastic. Respect!", name: "Lisa Anderson", role: "Hardcore Gamer", imageSrc:
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-hardcore-gamer-portrait-with-gaming-se-1773147108492-803fc87c.jpg", imageAlt: "Lisa Anderson testimonial"
},
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-hardcore-gamer-portrait-with-gaming-se-1773147108492-803fc87c.jpg", imageAlt: "Lisa Anderson testimonial"},
]}
textboxLayout="default"
useInvertedBackground={false}
@@ -270,17 +426,13 @@ export default function HomePage() {
tag="Global Platform"
metrics={[
{
id: "metric-1", value: "100+", description: "Free Games Available"
},
id: "metric-1", value: "100+", description: "Free Games Available"},
{
id: "metric-2", value: "50M+", description: "Active Players Monthly"
},
id: "metric-2", value: "50M+", description: "Active Players Monthly"},
{
id: "metric-3", value: "150K+", description: "Games Played Daily"
},
id: "metric-3", value: "150K+", description: "Games Played Daily"},
{
id: "metric-4", value: "98%", description: "Player Satisfaction"
},
id: "metric-4", value: "98%", description: "Player Satisfaction"},
]}
gridVariant="uniform-all-items-equal"
animationType="slide-up"

View File

@@ -0,0 +1,102 @@
"use client";
import React, { useEffect, useRef } from "react";
import { GameEngine, InputManager, SceneManager, GameConfig } from "@/utils/gameEngine";
export interface GameCanvasProps {
config: GameConfig;
onEngineReady?: (engine: GameEngine, inputManager: InputManager, sceneManager: SceneManager) => void;
className?: string;
ariaLabel?: string;
}
/**
* Reusable game canvas component with engine initialization
* Handles canvas rendering, game loop, and input management
*/
export const GameCanvas = React.forwardRef<HTMLCanvasElement, GameCanvasProps>(
({ config, onEngineReady, className = "", ariaLabel = "Game canvas" }, ref) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const engineRef = useRef<GameEngine | null>(null);
const inputManagerRef = useRef<InputManager | null>(null);
const sceneManagerRef = useRef<SceneManager | null>(null);
// Expose canvas ref
useEffect(() => {
if (ref) {
if (typeof ref === "function") {
ref(canvasRef.current);
} else {
ref.current = canvasRef.current;
}
}
}, [ref]);
// Initialize game engine
useEffect(() => {
if (!canvasRef.current) return;
try {
// Create engine
const engine = new GameEngine(config);
const initialized = engine.initialize(canvasRef.current);
if (!initialized) {
console.error("Failed to initialize game engine");
return;
}
// Create input and scene managers
const inputManager = new InputManager(canvasRef.current);
const sceneManager = new SceneManager();
// Store references
engineRef.current = engine;
inputManagerRef.current = inputManager;
sceneManagerRef.current = sceneManager;
// Initialize scene
sceneManager.createScene("main");
sceneManager.setActiveScene("main");
// Call callback if provided
if (onEngineReady) {
onEngineReady(engine, inputManager, sceneManager);
}
// Start game loop
engine.start();
// Handle window resize
const handleResize = () => {
if (canvasRef.current) {
engine.resize(canvasRef.current.offsetWidth, canvasRef.current.offsetHeight);
}
};
window.addEventListener("resize", handleResize);
// Cleanup
return () => {
window.removeEventListener("resize", handleResize);
engine.stop();
};
} catch (error) {
console.error("Game engine initialization error:", error);
}
}, [config, onEngineReady]);
return (
<canvas
ref={canvasRef}
className={`w-full h-full bg-black rounded-lg shadow-lg ${className}`}
aria-label={ariaLabel}
role="img"
/>
);
}
);
GameCanvas.displayName = "GameCanvas";
export default GameCanvas;

View File

@@ -0,0 +1,160 @@
"use client";
import React, { useEffect, useState } from "react";
import { GameEngine, InputManager, SceneManager, GameObject, GameConfig } from "@/utils/gameEngine";
import GameCanvas from "./GameCanvas";
export interface GameComponentProps {
gameConfig: GameConfig;
onInitialize?: (engine: GameEngine, input: InputManager, scene: SceneManager) => void;
onFrame?: (engine: GameEngine, input: InputManager) => void;
onCleanup?: () => void;
title?: string;
description?: string;
showStats?: boolean;
className?: string;
canvasClassName?: string;
}
/**
* Reusable game component wrapper
* Provides game loop integration, state management, and UI overlay
*/
export const GameComponent: React.FC<GameComponentProps> = ({
gameConfig,
onInitialize,
onFrame,
onCleanup,
title,
description,
showStats = false,
className = "", canvasClassName = ""}) => {
const [stats, setStats] = useState({
fps: 0,
deltaTime: 0,
elapsedTime: 0,
frameCount: 0,
});
const [isPaused, setIsPaused] = useState(false);
const engineRef = React.useRef<GameEngine | null>(null);
const inputRef = React.useRef<InputManager | null>(null);
const sceneRef = React.useRef<SceneManager | null>(null);
const statsIntervalRef = React.useRef<NodeJS.Timeout | null>(null);
const handleEngineReady = (engine: GameEngine, input: InputManager, scene: SceneManager) => {
engineRef.current = engine;
inputRef.current = input;
sceneRef.current = scene;
// Setup game update loop
engine.onUpdate((deltaTime: number) => {
if (onFrame && inputRef.current) {
onFrame(engine, inputRef.current);
}
// Update scene objects
const objects = scene.getActiveObjects();
objects.forEach((obj) => {
if (obj.active) {
obj.update(deltaTime);
}
});
});
// Setup rendering
engine.onRender((ctx) => {
// Render scene objects
const objects = scene.getActiveObjects();
objects.forEach((obj) => {
if (obj.visible) {
obj.render(ctx);
}
});
});
// Call user initialization callback
if (onInitialize) {
onInitialize(engine, input, scene);
}
// Setup stats updates
if (showStats) {
statsIntervalRef.current = setInterval(() => {
setStats({
fps: engine.state.fps,
deltaTime: engine.state.deltaTime,
elapsedTime: engine.state.elapsedTime,
frameCount: engine.state.frameCount,
});
}, 100);
}
};
const togglePause = () => {
if (!engineRef.current) return;
if (isPaused) {
engineRef.current.resume();
setIsPaused(false);
} else {
engineRef.current.pause();
setIsPaused(true);
}
};
useEffect(() => {
return () => {
if (statsIntervalRef.current) {
clearInterval(statsIntervalRef.current);
}
if (onCleanup) {
onCleanup();
}
};
}, [onCleanup]);
return (
<div className={`relative w-full h-full bg-gray-900 rounded-lg overflow-hidden ${className}`}>
<GameCanvas
config={gameConfig}
onEngineReady={handleEngineReady}
className={canvasClassName}
/>
{/* Game overlay UI */}
{(title || description || showStats || true) && (
<div className="absolute top-0 left-0 right-0 bottom-0 pointer-events-none flex flex-col justify-between p-4">
{/* Header */}
{(title || description) && (
<div className="pointer-events-auto">
{title && <h2 className="text-white text-lg font-bold">{title}</h2>}
{description && <p className="text-gray-300 text-sm">{description}</p>}
</div>
)}
{/* Controls */}
<div className="flex gap-2 pointer-events-auto">
<button
onClick={togglePause}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded transition"
aria-label={isPaused ? "Resume game" : "Pause game"}
>
{isPaused ? "Resume" : "Pause"}
</button>
</div>
{/* Stats Display */}
{showStats && (
<div className="pointer-events-auto text-right text-xs text-gray-400 font-mono space-y-1">
<div>FPS: {stats.fps}</div>
<div>Frame: {stats.frameCount}</div>
<div>Time: {stats.elapsedTime.toFixed(2)}s</div>
</div>
)}
</div>
)}
</div>
);
};
export default GameComponent;

388
src/utils/gameEngine.ts Normal file
View File

@@ -0,0 +1,388 @@
/**
* Game Engine Infrastructure
* Provides core game loop, state management, and canvas/WebGL rendering utilities
*/
export interface GameState {
isPaused: boolean;
isRunning: boolean;
deltaTime: number;
elapsedTime: number;
frameCount: number;
fps: number;
}
export interface GameConfig {
width: number;
height: number;
fps?: number;
useWebGL?: boolean;
}
export class GameEngine {
private canvas: HTMLCanvasElement | null = null;
private ctx: CanvasRenderingContext2D | WebGLRenderingContext | null = null;
private isWebGL: boolean = false;
private animationFrameId: number | null = null;
private lastFrameTime: number = 0;
private frameCount: number = 0;
private targetFPS: number = 60;
private accumulatedTime: number = 0;
state: GameState = {
isPaused: false,
isRunning: false,
deltaTime: 0,
elapsedTime: 0,
frameCount: 0,
fps: 0,
};
private updateCallbacks: Array<(deltaTime: number) => void> = [];
private renderCallbacks: Array<(ctx: CanvasRenderingContext2D | WebGLRenderingContext) => void> = [];
constructor(config: GameConfig) {
this.targetFPS = config.fps || 60;
this.isWebGL = config.useWebGL || false;
}
/**
* Initialize the game engine with a canvas element
*/
initialize(canvas: HTMLCanvasElement): boolean {
if (!canvas) return false;
this.canvas = canvas;
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
if (this.isWebGL) {
this.ctx = canvas.getContext('webgl') || canvas.getContext('webgl2');
if (!this.ctx) {
console.warn('WebGL not supported, falling back to 2D');
this.ctx = canvas.getContext('2d');
this.isWebGL = false;
}
} else {
this.ctx = canvas.getContext('2d');
}
return this.ctx !== null;
}
/**
* Register an update callback to be called each frame
*/
onUpdate(callback: (deltaTime: number) => void): void {
this.updateCallbacks.push(callback);
}
/**
* Register a render callback to be called each frame
*/
onRender(callback: (ctx: CanvasRenderingContext2D | WebGLRenderingContext) => void): void {
this.renderCallbacks.push(callback);
}
/**
* Start the game loop
*/
start(): void {
if (this.state.isRunning) return;
this.state.isRunning = true;
this.state.isPaused = false;
this.lastFrameTime = performance.now();
this.accumulatedTime = 0;
this.gameLoop();
}
/**
* Pause the game
*/
pause(): void {
this.state.isPaused = true;
}
/**
* Resume the game
*/
resume(): void {
this.state.isPaused = false;
this.lastFrameTime = performance.now();
}
/**
* Stop the game loop
*/
stop(): void {
this.state.isRunning = false;
this.state.isPaused = false;
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
}
/**
* Main game loop using requestAnimationFrame
*/
private gameLoop = (): void => {
if (!this.state.isRunning) return;
const currentTime = performance.now();
const frameDeltaTime = (currentTime - this.lastFrameTime) / 1000;
this.lastFrameTime = currentTime;
if (!this.state.isPaused) {
this.accumulatedTime += frameDeltaTime;
const frameTime = 1 / this.targetFPS;
// Update game state
while (this.accumulatedTime >= frameTime) {
this.state.deltaTime = frameTime;
this.state.elapsedTime += frameTime;
this.state.frameCount++;
// Call update callbacks
this.updateCallbacks.forEach(callback => callback(frameTime));
this.accumulatedTime -= frameTime;
}
// Calculate FPS
this.frameCount++;
if (currentTime - (this.lastFrameTime - frameDeltaTime) >= 1000) {
this.state.fps = this.frameCount;
this.frameCount = 0;
}
// Clear canvas
this.clearCanvas();
// Call render callbacks
if (this.ctx) {
this.renderCallbacks.forEach(callback => callback(this.ctx!));
}
}
this.animationFrameId = requestAnimationFrame(this.gameLoop);
};
/**
* Clear the canvas
*/
private clearCanvas(): void {
if (!this.canvas || !this.ctx) return;
if (this.isWebGL) {
const gl = this.ctx as WebGLRenderingContext;
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
} else {
const ctx = this.ctx as CanvasRenderingContext2D;
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
}
/**
* Get canvas dimensions
*/
getDimensions(): { width: number; height: number } {
return {
width: this.canvas?.width || 0,
height: this.canvas?.height || 0,
};
}
/**
* Resize canvas
*/
resize(width: number, height: number): void {
if (!this.canvas) return;
this.canvas.width = width;
this.canvas.height = height;
}
}
/**
* Base class for reusable game components
*/
export class GameObject {
x: number = 0;
y: number = 0;
width: number = 0;
height: number = 0;
rotation: number = 0;
scaleX: number = 1;
scaleY: number = 1;
visible: boolean = true;
active: boolean = true;
constructor(x: number = 0, y: number = 0, width: number = 0, height: number = 0) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
/**
* Update game object state
*/
update(deltaTime: number): void {
// Override in subclasses
}
/**
* Render game object
*/
render(ctx: CanvasRenderingContext2D | WebGLRenderingContext): void {
// Override in subclasses
}
/**
* Check if this object collides with another
*/
collidsWith(other: GameObject): boolean {
return (
this.x < other.x + other.width &&
this.x + this.width > other.x &&
this.y < other.y + other.height &&
this.y + this.height > other.y
);
}
/**
* Get bounds
*/
getBounds(): { x: number; y: number; width: number; height: number } {
return {
x: this.x,
y: this.y,
width: this.width,
height: this.height,
};
}
}
/**
* Input manager for handling keyboard and mouse input
*/
export class InputManager {
private keysPressed: Map<string, boolean> = new Map();
private mousePosition: { x: number; y: number } = { x: 0, y: 0 };
private mouseButtons: Map<number, boolean> = new Map();
constructor(canvas: HTMLCanvasElement) {
this.attachEventListeners(canvas);
}
private attachEventListeners(canvas: HTMLCanvasElement): void {
// Keyboard events
document.addEventListener('keydown', (e) => {
this.keysPressed.set(e.key.toLowerCase(), true);
});
document.addEventListener('keyup', (e) => {
this.keysPressed.set(e.key.toLowerCase(), false);
});
// Mouse events
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
this.mousePosition = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
};
});
canvas.addEventListener('mousedown', (e) => {
this.mouseButtons.set(e.button, true);
});
canvas.addEventListener('mouseup', (e) => {
this.mouseButtons.set(e.button, false);
});
}
/**
* Check if a key is pressed
*/
isKeyPressed(key: string): boolean {
return this.keysPressed.get(key.toLowerCase()) || false;
}
/**
* Get mouse position
*/
getMousePosition(): { x: number; y: number } {
return this.mousePosition;
}
/**
* Check if a mouse button is pressed
*/
isMouseButtonPressed(button: number = 0): boolean {
return this.mouseButtons.get(button) || false;
}
}
/**
* Scene manager for managing game scenes
*/
export class SceneManager {
private scenes: Map<string, GameObject[]> = new Map();
private activeScene: string | null = null;
/**
* Create a new scene
*/
createScene(name: string): void {
this.scenes.set(name, []);
}
/**
* Set active scene
*/
setActiveScene(name: string): boolean {
if (this.scenes.has(name)) {
this.activeScene = name;
return true;
}
return false;
}
/**
* Add object to active scene
*/
addObject(obj: GameObject): void {
if (this.activeScene) {
const scene = this.scenes.get(this.activeScene);
if (scene) {
scene.push(obj);
}
}
}
/**
* Get all active objects
*/
getActiveObjects(): GameObject[] {
if (this.activeScene) {
return this.scenes.get(this.activeScene) || [];
}
return [];
}
/**
* Clear active scene
*/
clearActiveScene(): void {
if (this.activeScene) {
this.scenes.set(this.activeScene, []);
}
}
}