import React, { useState, useEffect, useRef } from 'react';
import { initializeApp } from 'firebase/app';
import {
getAuth,
signInAnonymously,
updateProfile,
onAuthStateChanged
} from 'firebase/auth';
import {
getFirestore,
doc,
setDoc,
updateDoc,
deleteDoc,
collection,
onSnapshot
} from 'firebase/firestore';
// --- FIREBASE CONFIGURATION ---
// Replace with your Firebase project config credentials
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_PROJECT.firebaseapp.com",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_PROJECT.appspot.com",
messagingSenderId: "YOUR_SENDER_ID",
appId: "YOUR_APP_ID"
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);
const ARENA_WIDTH = 2000;
const ARENA_HEIGHT = 2000;
const PLAYER_RADIUS = 20;
const MOVE_SPEED = 5;
// Helper to generate a random hex color for the player
const getRandomColor = () => {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
};
export default function Game() {
const [user, setUser] = useState(null);
const [displayName, setDisplayName] = useState('');
const [isJoined, setIsJoined] = useState(false);
const [suspectedPlayer, setSuspectedPlayer] = useState(null);
const [allPlayers, setAllPlayers] = useState({});
const canvasRef = useRef(null);
const keysPressed = useRef({});
const positionRef = useRef({ x: 100, y: 100 });
const animationFrameId = useRef(null);
// 1. Listen for Firebase Auth changes
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
setUser(currentUser);
});
return () => unsubscribe();
}, []);
// 2. Real-time Firestore subscription for all active players
useEffect(() => {
const playersRef = collection(db, 'players');
const unsubscribe = onSnapshot(playersRef, (snapshot) => {
const playersData = {};
snapshot.forEach((docSnap) => {
playersData[docSnap.id] = docSnap.data();
});
setAllPlayers(playersData);
});
return () => unsubscribe();
}, []);
// 3. Handle Keyboard Controls & Local Position Syncing
useEffect(() => {
if (!isJoined || !user) return;
const handleKeyDown = (e) => {
keysPressed.current[e.key.toLowerCase()] = true;
};
const handleKeyUp = (e) => {
keysPressed.current[e.key.toLowerCase()] = false;
};
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
// Main update loop for movement
let lastUpdateTime = 0;
const gameLoop = (timestamp) => {
let dx = 0;
let dy = 0;
if (keysPressed.current['w'] || keysPressed.current['arrowup']) dy -= MOVE_SPEED;
if (keysPressed.current['s'] || keysPressed.current['arrowdown']) dy += MOVE_SPEED;
if (keysPressed.current['a'] || keysPressed.current['arrowleft']) dx -= MOVE_SPEED;
if (keysPressed.current['d'] || keysPressed.current['arrowright']) dx += MOVE_SPEED;
if (dx !== 0 || dy !== 0) {
let newX = positionRef.current.x + dx;
let newY = positionRef.current.y + dy;
// Keep inside boundary
newX = Math.max(PLAYER_RADIUS, Math.min(ARENA_WIDTH - PLAYER_RADIUS, newX));
newY = Math.max(PLAYER_RADIUS, Math.min(ARENA_HEIGHT - PLAYER_RADIUS, newY));
positionRef.current = { x: newX, y: newY };
// Throttle Firestore updates (~30fps write rate to stay within standard limits)
if (timestamp - lastUpdateTime > 33) {
updateDoc(doc(db, 'players', user.uid), {
x: newX,
y: newY
}).catch(console.error);
lastUpdateTime = timestamp;
}
}
animationFrameId.current = requestAnimationFrame(gameLoop);
};
animationFrameId.current = requestAnimationFrame(gameLoop);
// Clean up document on exit or unmount
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
cancelAnimationFrame(animationFrameId.current);
if (user) {
deleteDoc(doc(db, 'players', user.uid)).catch(console.error);
}
};
}, [isJoined, user]);
// 4. Render Canvas (Arena, Players, Names, Camera focus)
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
// Resize canvas to window dimensions
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Determine camera focus position: joined player or spectator suspect target
let cameraTargetX = ARENA_WIDTH / 2;
let cameraTargetY = ARENA_HEIGHT / 2;
if (isJoined) {
cameraTargetX = positionRef.current.x;
cameraTargetY = positionRef.current.y;
} else if (suspectedPlayer && allPlayers[suspectedPlayer]) {
cameraTargetX = allPlayers[suspectedPlayer].x;
cameraTargetY = allPlayers[suspectedPlayer].y;
}
ctx.save();
// Center camera on target
ctx.translate(canvas.width / 2 - cameraTargetX, canvas.height / 2 - cameraTargetY);
// Draw Arena Boundary
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 5;
ctx.strokeRect(0, 0, ARENA_WIDTH, ARENA_HEIGHT);
// Draw Arena Background Grid
ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
ctx.lineWidth = 1;
for (let x = 0; x < ARENA_WIDTH; x += 100) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, ARENA_HEIGHT);
ctx.stroke();
}
for (let y = 0; y < ARENA_HEIGHT; y += 100) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(ARENA_WIDTH, y);
ctx.stroke();
}
// Draw All Players
Object.entries(allPlayers).forEach(([id, player]) => {
// Draw Circle Character
ctx.beginPath();
ctx.arc(player.x, player.y, PLAYER_RADIUS, 0, Math.PI * 2);
ctx.fillStyle = player.color || '#333';
ctx.fill();
// Outline active player / suspected target
if (id === user?.uid || id === suspectedPlayer) {
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 3;
ctx.stroke();
}
// Draw Name above Player
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(player.name || 'Anonymous', player.x, player.y - PLAYER_RADIUS - 8);
});
ctx.restore();
}, [allPlayers, isJoined, suspectedPlayer, user]);
// Join match handler
const handleJoinGame = async (e) => {
e.preventDefault();
if (!displayName.trim()) return;
let currentUser = user;
if (!currentUser) {
const userCred = await signInAnonymously(auth);
currentUser = userCred.user;
}
await updateProfile(currentUser, { displayName });
const spawnX = Math.floor(Math.random() * (ARENA_WIDTH - 200)) + 100;
const spawnY = Math.floor(Math.random() * (ARENA_HEIGHT - 200)) + 100;
const color = getRandomColor();
positionRef.current = { x: spawnX, y: spawnY };
await setDoc(doc(db, 'players', currentUser.uid), {
name: displayName,
x: spawnX,
y: spawnY,
color: color,
joinedAt: Date.now()
});
setSuspectedPlayer(null);
setIsJoined(true);
};
// Leave match back to Lobby/Spectator mode
const handleLeaveGame = async () => {
if (user) {
await deleteDoc(doc(db, 'players', user.uid));
}
setIsJoined(false);
};
return (
{/* Spectator UI overlay when connected */}
{isJoined && (
<div style={styles.hud}>
<button style={styles.leaveButton} onClick={handleLeaveGame}>
Exit to Lobby / Spectate
</button>
</div>
)}
{/* Lobby / Spectator Overlay */}
{!isJoined && (
<div style={styles.overlay}>
<div style={styles.lobbyCard}>
<h1 style={styles.title}>Arena Lobby</h1>
<form onSubmit={handleJoinGame} style={styles.form}>
<input
type="text"
placeholder="Enter Display Name"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
maxLength={15}
style={styles.input}
required
/>
<button type="submit" style={styles.joinButton}>
Join Arena
</button>
</form>
<div style={styles.spectatorSection}>
<h3 style={styles.subtitle}>Spectate / Suspect Players</h3>
{Object.keys(allPlayers).length === 0 ? (
<p style={styles.emptyText}>No active players in arena</p>
) : (
<div style={styles.playerList}>
{Object.entries(allPlayers).map(([id, p]) => (
<button
key={id}
onClick={() => setSuspectedPlayer(id)}
style={{
...styles.suspectButton,
borderColor: suspectedPlayer === id ? p.color : 'transparent',
backgroundColor: suspectedPlayer === id ? 'rgba(255, 255, 255, 0.2)' : 'rgba(255, 255, 255, 0.05)'
}}
>
<span style={{ ...styles.colorBadge, backgroundColor: p.color }} />
{p.name} {suspectedPlayer === id ? '(Suspecting)' : ''}
</button>
))}
</div>
)}
</div>
</div>
</div>
)}
</div>
);
}
// Inline styles for complete self-containment
const styles = {
container: {
position: 'relative',
width: '100vw',
height: '100vh',
backgroundColor: '#121212',
overflow: 'hidden',
fontFamily: 'sans-serif'
},
canvas: {
display: 'block'
},
hud: {
position: 'absolute',
top: 20,
left: 20,
zIndex: 10
},
leaveButton: {
padding: '10px 16px',
backgroundColor: '#ff4444',
color: '#fff',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontWeight: 'bold'
},
overlay: {
position: 'absolute',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.65)',
zIndex: 10
},
lobbyCard: {
backgroundColor: '#1e1e2f',
padding: '30px',
borderRadius: '12px',
width: '360px',
boxShadow: '0 8px 24px rgba(0,0,0,0.5)',
color: '#fff',
textAlign: 'center'
},
title: {
margin: '0 0 20px 0',
fontSize: '24px'
},
form: {
display: 'flex',
flexDirection: 'column',
gap: '12px'
},
input: {
padding: '12px',
borderRadius: '6px',
border: '1px solid #333',
backgroundColor: '#2b2b3d',
color: '#fff',
fontSize: '16px',
outline: 'none'
},
joinButton: {
padding: '12px',
borderRadius: '6px',
border: 'none',
backgroundColor: '#4CAF50',
color: '#fff',
fontSize: '16px',
fontWeight: 'bold',
cursor: 'pointer'
},
spectatorSection: {
marginTop: '25px',
textAlign: 'left'
},
subtitle: {
fontSize: '14px',
textTransform: 'uppercase',
color: '#aaa',
marginBottom: '10px'
},
emptyText: {
fontSize: '12px',
color: '#666'
},
playerList: {
display: 'flex',
flexDirection: 'column',
gap: '8px',
maxHeight: '180px',
overflowY: 'auto'
},
suspectButton: {
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '8px 12px',
borderWidth: '2px',
borderStyle: 'solid',
borderRadius: '6px',
color: '#fff',
cursor: 'pointer',
fontSize: '14px',
textAlign: 'left'
},
colorBadge: {
width: '12px',
height: '12px',
borderRadius: '50%',
display: 'inline-block'
}
};
import React, { useState, useEffect, useRef } from 'react';
import { initializeApp } from 'firebase/app';
import {
getAuth,
signInAnonymously,
updateProfile,
onAuthStateChanged
} from 'firebase/auth';
import {
getFirestore,
doc,
setDoc,
updateDoc,
deleteDoc,
collection,
onSnapshot
} from 'firebase/firestore';
// --- FIREBASE CONFIGURATION ---
// Replace with your Firebase project config credentials
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_PROJECT.firebaseapp.com",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_PROJECT.appspot.com",
messagingSenderId: "YOUR_SENDER_ID",
appId: "YOUR_APP_ID"
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);
const ARENA_WIDTH = 2000;
const ARENA_HEIGHT = 2000;
const PLAYER_RADIUS = 20;
const MOVE_SPEED = 5;
// Helper to generate a random hex color for the player
const getRandomColor = () => {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
};
export default function Game() {
const [user, setUser] = useState(null);
const [displayName, setDisplayName] = useState('');
const [isJoined, setIsJoined] = useState(false);
const [suspectedPlayer, setSuspectedPlayer] = useState(null);
const [allPlayers, setAllPlayers] = useState({});
const canvasRef = useRef(null);
const keysPressed = useRef({});
const positionRef = useRef({ x: 100, y: 100 });
const animationFrameId = useRef(null);
// 1. Listen for Firebase Auth changes
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
setUser(currentUser);
});
return () => unsubscribe();
}, []);
// 2. Real-time Firestore subscription for all active players
useEffect(() => {
const playersRef = collection(db, 'players');
const unsubscribe = onSnapshot(playersRef, (snapshot) => {
const playersData = {};
snapshot.forEach((docSnap) => {
playersData[docSnap.id] = docSnap.data();
});
setAllPlayers(playersData);
});
}, []);
// 3. Handle Keyboard Controls & Local Position Syncing
useEffect(() => {
if (!isJoined || !user) return;
}, [isJoined, user]);
// 4. Render Canvas (Arena, Players, Names, Camera focus)
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
}, [allPlayers, isJoined, suspectedPlayer, user]);
// Join match handler
const handleJoinGame = async (e) => {
e.preventDefault();
if (!displayName.trim()) return;
};
// Leave match back to Lobby/Spectator mode
const handleLeaveGame = async () => {
if (user) {
await deleteDoc(doc(db, 'players', user.uid));
}
setIsJoined(false);
};
return (
);
}
// Inline styles for complete self-containment
const styles = {
container: {
position: 'relative',
width: '100vw',
height: '100vh',
backgroundColor: '#121212',
overflow: 'hidden',
fontFamily: 'sans-serif'
},
canvas: {
display: 'block'
},
hud: {
position: 'absolute',
top: 20,
left: 20,
zIndex: 10
},
leaveButton: {
padding: '10px 16px',
backgroundColor: '#ff4444',
color: '#fff',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontWeight: 'bold'
},
overlay: {
position: 'absolute',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.65)',
zIndex: 10
},
lobbyCard: {
backgroundColor: '#1e1e2f',
padding: '30px',
borderRadius: '12px',
width: '360px',
boxShadow: '0 8px 24px rgba(0,0,0,0.5)',
color: '#fff',
textAlign: 'center'
},
title: {
margin: '0 0 20px 0',
fontSize: '24px'
},
form: {
display: 'flex',
flexDirection: 'column',
gap: '12px'
},
input: {
padding: '12px',
borderRadius: '6px',
border: '1px solid #333',
backgroundColor: '#2b2b3d',
color: '#fff',
fontSize: '16px',
outline: 'none'
},
joinButton: {
padding: '12px',
borderRadius: '6px',
border: 'none',
backgroundColor: '#4CAF50',
color: '#fff',
fontSize: '16px',
fontWeight: 'bold',
cursor: 'pointer'
},
spectatorSection: {
marginTop: '25px',
textAlign: 'left'
},
subtitle: {
fontSize: '14px',
textTransform: 'uppercase',
color: '#aaa',
marginBottom: '10px'
},
emptyText: {
fontSize: '12px',
color: '#666'
},
playerList: {
display: 'flex',
flexDirection: 'column',
gap: '8px',
maxHeight: '180px',
overflowY: 'auto'
},
suspectButton: {
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '8px 12px',
borderWidth: '2px',
borderStyle: 'solid',
borderRadius: '6px',
color: '#fff',
cursor: 'pointer',
fontSize: '14px',
textAlign: 'left'
},
colorBadge: {
width: '12px',
height: '12px',
borderRadius: '50%',
display: 'inline-block'
}
};