Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7b874ad
[Issue-10] Implement Reusable <GameCanvas> Component for Scalable Gam…
Bana0615 Aug 5, 2025
395c4d1
[Issue-16] Paddle Battle: Added local storage to store stats
Bana0615 Aug 5, 2025
e692407
[Issue-16] Paddle Battle: Game can be restarted with spacebar now
Bana0615 Aug 5, 2025
d0ff5cf
[Issue-16] Paddle Battle: Implemented pause functionality
Bana0615 Aug 5, 2025
ace5d0e
[Issue-16] Paddle Battle: Implemented new stats gamesPlayed, playerLo…
Bana0615 Aug 5, 2025
78b5300
[Issue-10] Added border to games and made the controls and objectives…
Bana0615 Aug 5, 2025
c18c002
[Issue-10] Created a player stats section
Bana0615 Aug 5, 2025
33c990b
[Issue-10] Stats section now updates when stats do
Bana0615 Aug 5, 2025
cb629dd
[Issue-16] Paddle Battle: Added a start screen
Bana0615 Aug 5, 2025
0a466ba
[Issue-16] Paddle Battle: Converted the game to a buffalo theme
Bana0615 Aug 5, 2025
edd6b7e
[Issue-10] updated slugs for original and arcade games
Bana0615 Aug 5, 2025
1ff914a
[Issue-10] Each game now has a custom meta description for better seo
Bana0615 Aug 5, 2025
d935d59
[Issue-16] Paddle Battle: Added a difficulty setting
Bana0615 Aug 5, 2025
11175dd
[Issue-16] Paddle Battle: centered difficulties
Bana0615 Aug 5, 2025
73fed7a
[Issue-16] Paddle Battle: Cleaned up start page
Bana0615 Aug 5, 2025
c619cd7
[Issue-16] Fixed linting errors and setup a way not show games on mob…
Bana0615 Aug 6, 2025
2c83556
Emptied test data for obl original games
Bana0615 Aug 6, 2025
0020d04
[Issue-16] Paddle Battle: Updated image for listing
Bana0615 Aug 6, 2025
f4af25d
Fixed loading issue
Bana0615 Aug 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 8 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,22 @@
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"next": "15.4.5",
"phaser": "^3.90.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-ga4": "^2.1.0",
"next": "15.4.5"
"react-ga4": "^2.1.0"
},
"devDependencies": {
"typescript": "^5",
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4",
"eslint": "^9",
"eslint-config-next": "15.4.5",
"@eslint/eslintrc": "^3",
"next-sitemap": "^4.2.3"
"next-sitemap": "^4.2.3",
"tailwindcss": "^4",
"typescript": "^5"
}
}
Binary file added public/images/games/paddle-battle.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 42 additions & 0 deletions src/app/games/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { Metadata } from 'next';
import { getGameDetailsBySlug, getAllGameSlugs } from '@/games/game-meta-loader';
import { generateMetadata as generatePageMetadata } from '@/utils/metadata';
import GameClient from '@/components/games/GameClient';

interface GamePageProps {
params: Promise<{ slug: string[] }>;
}

export async function generateMetadata({ params }: GamePageProps): Promise<Metadata> {
const { slug } = await params;
const gameSlug = slug[slug.length - 1];
const fullPath = slug.join('/');
const gameDetails = getGameDetailsBySlug(gameSlug);

return generatePageMetadata({
title: gameDetails.title,
description: gameDetails.metaDescription,
urlPath: `/games/${fullPath}`,
});
}

export async function generateStaticParams() {
return getAllGameSlugs();
}

export default async function GamePage({ params }: GamePageProps) {
const { slug } = await params;
const gameSlug = slug[slug.length - 1];
const gameDetails = getGameDetailsBySlug(gameSlug);

return (
<GameClient
slug={gameSlug}
title={gameDetails.title}
description={gameDetails.description}
controls={gameDetails.controls}
stats={gameDetails.stats}
isDesktopOnly={gameDetails.isDesktopOnly} // Pass the new prop
/>
);
}
14 changes: 8 additions & 6 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@ export default function HomePage() {
<HubsSection />

{/* Originals Section */}
<FeaturedGamesSection
title="One Buffalo Originals"
games={originalGamesData}
accentColor="red"
browseAllLink="/games?filter=originals"
/>
{originalGamesData.length > 0 && (
<FeaturedGamesSection
title="One Buffalo Originals"
games={originalGamesData}
accentColor="red"
browseAllLink="/games?filter=originals"
/>
)}

{/* Arcade Section */}
<FeaturedGamesSection
Expand Down
40 changes: 40 additions & 0 deletions src/components/games/GameCanvas.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use client';

import { useEffect, useRef } from 'react';
import * as Phaser from 'phaser';

// Define the props for the component
interface GameCanvasProps {
gameConfig: Phaser.Types.Core.GameConfig;
}

/**
* A reusable React component to host and manage a Phaser game instance.
* It handles the creation and destruction of the game, preventing memory leaks.
*/
export default function GameCanvas({ gameConfig }: GameCanvasProps) {
// Use a ref to hold the Phaser game instance
const gameRef = useRef<Phaser.Game | null>(null);

// The main effect to manage the game's lifecycle
useEffect(() => {
// Ensure this only runs in the browser
if (typeof window !== 'undefined') {
// Initialize the Phaser game when the component mounts
gameRef.current = new Phaser.Game({
...gameConfig,
parent: 'game-container', // Tell Phaser where to inject the canvas
});
}

// The cleanup function is critical for SPAs
return () => {
// Destroy the game instance when the component unmounts
gameRef.current?.destroy(true);
gameRef.current = null;
};
}, [gameConfig]); // Re-run the effect if the gameConfig prop changes

// This div is the target where Phaser will inject the game canvas
return <div id="game-container" className="w-full flex justify-center" />;
}
125 changes: 125 additions & 0 deletions src/components/games/GameClient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
'use client';

import { useState, useEffect, Suspense, useCallback } from 'react';
import dynamic from 'next/dynamic';
import ArcadeButton from '@/components/ArcadeButton';
import type { IGameConfig } from '@/games/game-loader';
import type { GameStat } from '@/types';
import * as statsManager from '@/utils/statsManager';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faDesktop } from '@fortawesome/free-solid-svg-icons';

// A responsive placeholder that maintains the correct aspect ratio
const GameLoadingSkeleton = () => (
<div className="w-full max-w-[800px] aspect-[4/3] bg-foreground/10 animate-pulse rounded-lg" />
);

const GameCanvas = dynamic(() => import('@/components/games/GameCanvas'), {
ssr: false,
loading: () => <GameLoadingSkeleton />,
});

interface GameClientProps {
slug: string;
title: string;
description: string;
controls: string[];
stats: GameStat[];
isDesktopOnly: boolean;
}

export default function GameClient({
slug,
title,
description,
controls,
stats,
isDesktopOnly,
}: GameClientProps) {
const [gameConfig, setGameConfig] = useState<IGameConfig | null>(null);
const [playerStats, setPlayerStats] = useState<Record<string, number>>({});

const refreshStats = useCallback(() => {
const allStats = statsManager.getAllStats(slug);
setPlayerStats(allStats);
}, [slug]);

useEffect(() => {
import('@/games/game-loader').then(({ getGameConfigBySlug }) => {
const config = getGameConfigBySlug(slug);
setGameConfig(config);
});

refreshStats();
window.addEventListener('statsUpdated', refreshStats);
return () => {
window.removeEventListener('statsUpdated', refreshStats);
};
}, [slug, refreshStats]);

return (
<div className="relative bg-obl-dark-blue/95 scanline-overlay text-white min-h-screen py-16">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h1 className="font-press-start text-4xl md:text-5xl mb-8 animate-glow">{title}</h1>

<div className="mb-8">
<div
className={`${
isDesktopOnly ? 'hidden md:inline-block' : 'inline-block'
} w-full max-w-[816px] border-4 border-obl-blue rounded-lg p-1 shadow-lg bg-black`}>
<div className="flex justify-center">
<Suspense fallback={<GameLoadingSkeleton />}>
{gameConfig ? <GameCanvas gameConfig={gameConfig} /> : <GameLoadingSkeleton />}
</Suspense>
</div>
</div>

{isDesktopOnly && (
<div className="block md:hidden border-4 border-obl-red rounded-lg p-8 max-w-md mx-auto">
<FontAwesomeIcon icon={faDesktop} className="h-16 w-16 text-obl-red mb-4" />
<h2 className="font-orbitron text-2xl font-bold mb-4">Desktop Recommended</h2>
<p className="font-mono text-gray-300">
This game is controlled by the keyboard and is best experienced on a desktop
computer.
</p>
</div>
)}
</div>

<div className="max-w-2xl mx-auto text-left font-mono text-gray-300 space-y-8">
<div>
<h2 className="font-orbitron text-2xl font-bold text-obl-red mb-4">
Controls & Objective
</h2>
{controls.map((control, index) => (
<p key={index} dangerouslySetInnerHTML={{ __html: control }} />
))}
<p>{description}</p>
</div>

{stats && stats.length > 0 && (
<div>
<h2 className="font-orbitron text-2xl font-bold text-obl-red mb-4">Player Stats</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
{stats.map((stat) => (
<div key={stat.key} className="bg-obl-blue/20 p-3 rounded-md">
<div className="text-gray-400 text-sm">{stat.label}</div>
<div className="text-white font-bold text-2xl">
{playerStats[stat.key] || 0}
</div>
</div>
))}
</div>
</div>
)}
</div>

<div className="mt-12">
<ArcadeButton href="/games" color="blue">
Back to Arcade
</ArcadeButton>
</div>
</div>
</div>
);
}
56 changes: 44 additions & 12 deletions src/data/arcadeGames.json
Original file line number Diff line number Diff line change
@@ -1,34 +1,66 @@
[
{
"title": "Paddle Battle",
"imageUrl": "/images/games/paddle-battle.webp",
"linkUrl": "/games/arcade/paddle-battle",
"metaDescription": "Play Paddle Battle, a Buffalo-themed tribute to the original paddle-and-ball arcade games. Features unique snowflake physics and full stat tracking against an AI opponent.",
"isDesktopOnly": true,
"isNew": true,
"tags": ["arcade", "classic"],
"releaseDate": "2025-08-05",
"popularity": 95,
"description": "First to 10 points wins!",
"controls": [
"Use the <span class='text-white font-bold'>UP</span> and <span class='text-white font-bold'>DOWN</span> arrow keys to move your paddle.",
"Press <span class='text-white font-bold'>P</span> to pause the game.",
"Press <span class='text-white font-bold'>SPACE</span> to restart after game over."
],
"stats": [
{ "key": "gamesPlayed", "label": "Games Played" },
{ "key": "playerWins", "label": "Wins" },
{ "key": "playerLosses", "label": "Losses" },
{ "key": "longestRally", "label": "Longest Rally" },
{ "key": "totalRallies", "label": "Total Paddle Hits" },
{ "key": "totalPlayTime", "label": "Play Time (s)" }
]
},
{
"title": "Galaxy Invaders",
"imageUrl": "https://placehold.co/300x200/e7042d/003091?text=Arcade+Classic",
"linkUrl": "#",
"linkUrl": "/games/arcade/galaxy-invaders",
"metaDescription": "",
"isDesktopOnly": true,
"isComingSoon": true,
"tags": ["arcade", "shooter"],
"releaseDate": "2024-10-25",
"popularity": 90
"popularity": 90,
"description": "",
"controls": []
},
{
"title": "Block Breaker",
"imageUrl": "https://placehold.co/300x200/e7042d/003091?text=Arcade+Classic",
"linkUrl": "#",
"metaDescription": "",
"isDesktopOnly": true,
"isComingSoon": true,
"tags": ["arcade", "puzzle"],
"releaseDate": "2024-09-11",
"popularity": 88
},
{
"title": "Road Racer",
"imageUrl": "https://placehold.co/300x200/e7042d/003091?text=Arcade+Classic",
"linkUrl": "#",
"tags": ["arcade", "racing"],
"releaseDate": "2024-11-05",
"popularity": 75
"popularity": 88,
"description": "",
"controls": []
},
{
"title": "Maze Mania",
"imageUrl": "https://placehold.co/300x200/e7042d/003091?text=Arcade+Classic",
"linkUrl": "#",
"metaDescription": "",
"isDesktopOnly": true,
"isComingSoon": true,
"tags": ["arcade", "puzzle", "strategy"],
"releaseDate": "2024-08-19",
"popularity": 82
"popularity": 82,
"description": "",
"controls": []
}
]
Loading