Skip to content

generic shooting game #500

Description

@device26oistp-alt

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'
}
};

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions