Spaces:
Running
Running
| // Help menu toggle | |
| document.getElementById('closeHelp').addEventListener('click', () => { | |
| document.getElementById('helpMenu').classList.add('hidden'); | |
| }); | |
| // Game constants | |
| const CANVAS_WIDTH = 2000; | |
| const CANVAS_HEIGHT = 2000; | |
| const PLAYER_SIZE = 30; | |
| const BULLET_SIZE = 8; | |
| const FOOD_SIZE = 10; | |
| const MAX_PLAYERS = 50; | |
| const MAX_FOOD = 500; | |
| const MAX_AI = 20; | |
| // Game state | |
| let gameState = { | |
| players: {}, | |
| bullets: [], | |
| food: [], | |
| aiTanks: [], | |
| playerId: null, | |
| score: 0, | |
| level: 1, | |
| upgrades: { | |
| health: 0, | |
| damage: 0, | |
| reload: 0, | |
| movement: 0, | |
| bulletSpeed: 0, | |
| penetration: 0 | |
| }, | |
| upgradePoints: 0 | |
| }; | |
| // Canvas setup | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| // Viewport tracking | |
| let viewport = { | |
| x: 0, | |
| y: 0, | |
| width: canvas.width, | |
| height: canvas.height, | |
| target: { x: 0, y: 0 } | |
| }; | |
| // Game loop | |
| function gameLoop() { | |
| update(); | |
| render(); | |
| requestAnimationFrame(gameLoop); | |
| } | |
| function update() { | |
| // Update viewport to follow player | |
| if (gameState.playerId && gameState.players[gameState.playerId]) { | |
| const player = gameState.players[gameState.playerId]; | |
| viewport.target.x = player.x - viewport.width / 2; | |
| viewport.target.y = player.y - viewport.height / 2; | |
| // Smooth viewport movement | |
| viewport.x += (viewport.target.x - viewport.x) * 0.1; | |
| viewport.y += (viewport.target.y - viewport.y) * 0.1; | |
| } | |
| } | |
| function render() { | |
| // Clear canvas | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Draw grid background | |
| drawGrid(); | |
| // Draw all game objects relative to viewport | |
| drawFood(); | |
| drawBullets(); | |
| drawPlayers(); | |
| drawAI(); | |
| // Draw UI | |
| drawUI(); | |
| } | |
| function drawGrid() { | |
| const gridSize = 100; | |
| const offsetX = -viewport.x % gridSize; | |
| const offsetY = -viewport.y % gridSize; | |
| // Draw a subtle grid with slightly more visible lines | |
| ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)'; | |
| ctx.lineWidth = 1.5; | |
| // Vertical lines | |
| for (let x = offsetX; x < canvas.width; x += gridSize) { | |
| ctx.beginPath(); | |
| ctx.moveTo(x, 0); | |
| ctx.lineTo(x, canvas.height); | |
| ctx.stroke(); | |
| } | |
| // Horizontal lines | |
| for (let y = offsetY; y < canvas.height; y += gridSize) { | |
| ctx.beginPath(); | |
| ctx.moveTo(0, y); | |
| ctx.lineTo(canvas.width, y); | |
| ctx.stroke(); | |
| } | |
| } | |
| function drawPlayers() { | |
| for (const id in gameState.players) { | |
| const player = gameState.players[id]; | |
| const isCurrentPlayer = id === gameState.playerId; | |
| // Calculate screen position | |
| const screenX = player.x - viewport.x; | |
| const screenY = player.y - viewport.y; | |
| // Draw player tank | |
| ctx.save(); | |
| ctx.translate(screenX, screenY); | |
| ctx.rotate(player.angle); | |
| // Tank body | |
| ctx.fillStyle = isCurrentPlayer ? '#4F46E5' : player.color; | |
| ctx.beginPath(); | |
| ctx.rect(-PLAYER_SIZE/2, -PLAYER_SIZE/2, PLAYER_SIZE, PLAYER_SIZE); | |
| ctx.fill(); | |
| // Tank barrel | |
| ctx.fillStyle = '#D1D5DB'; | |
| ctx.beginPath(); | |
| ctx.rect(PLAYER_SIZE/2 - 5, -3, 20, 6); | |
| ctx.fill(); | |
| ctx.restore(); | |
| // Draw player name and health | |
| if (isCurrentPlayer || Math.random() < 0.1) { // Only draw names occasionally for performance | |
| ctx.fillStyle = '#FFFFFF'; | |
| ctx.font = '12px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText(player.name, screenX, screenY - PLAYER_SIZE - 5); | |
| // Health bar | |
| const healthWidth = 30; | |
| const healthPercent = player.health / player.maxHealth; | |
| ctx.fillStyle = '#FF0000'; | |
| ctx.fillRect(screenX - healthWidth/2, screenY - PLAYER_SIZE - 15, healthWidth, 3); | |
| ctx.fillStyle = '#00FF00'; | |
| ctx.fillRect(screenX - healthWidth/2, screenY - PLAYER_SIZE - 15, healthWidth * healthPercent, 3); | |
| } | |
| } | |
| } | |
| function drawBullets() { | |
| for (const bullet of gameState.bullets) { | |
| const screenX = bullet.x - viewport.x; | |
| const screenY = bullet.y - viewport.y; | |
| ctx.fillStyle = bullet.color; | |
| ctx.beginPath(); | |
| ctx.arc(screenX, screenY, BULLET_SIZE, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| } | |
| function drawFood() { | |
| ctx.shadowBlur = 5; | |
| for (const food of gameState.food) { | |
| const screenX = food.x - viewport.x; | |
| const screenY = food.y - viewport.y; | |
| // Only draw food that's visible in viewport | |
| if (screenX > -50 && screenX < canvas.width + 50 && | |
| screenY > -50 && screenY < canvas.height + 50) { | |
| ctx.shadowColor = food.color; | |
| ctx.fillStyle = food.color; | |
| ctx.beginPath(); | |
| ctx.arc(screenX, screenY, food.size, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| } | |
| ctx.shadowBlur = 0; | |
| } | |
| function drawAI() { | |
| // Similar to drawPlayers but with different styling | |
| } | |
| function drawUI() { | |
| // Draw current player stats | |
| if (gameState.playerId && gameState.players[gameState.playerId]) { | |
| const player = gameState.players[gameState.playerId]; | |
| // Mini stats in top left with better visibility | |
| ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; | |
| ctx.fillRect(10, 10, 180, 100); | |
| ctx.strokeStyle = '#4F46E5'; | |
| ctx.lineWidth = 2; | |
| ctx.strokeRect(10, 10, 180, 100); | |
| ctx.fillStyle = '#FFFFFF'; | |
| ctx.font = 'bold 16px Arial'; | |
| ctx.textAlign = 'left'; | |
| ctx.fillText(`Player: ${player.name}`, 20, 30); | |
| ctx.fillText(`Score: ${gameState.score}`, 20, 55); | |
| ctx.fillText(`Level: ${gameState.level}`, 20, 80); | |
| ctx.fillText(`Upgrades: ${gameState.upgradePoints}`, 20, 105); | |
| // Show help prompt if help menu is hidden | |
| if (document.getElementById('helpMenu').classList.contains('hidden')) { | |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; | |
| ctx.font = '14px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText('Press H for help', canvas.width/2, 30); | |
| } | |
| } | |
| } | |
| // Initialize game | |
| function initGame() { | |
| // Generate initial food with brighter colors | |
| for (let i = 0; i < MAX_FOOD; i++) { | |
| gameState.food.push({ | |
| x: Math.random() * CANVAS_WIDTH, | |
| y: Math.random() * CANVAS_HEIGHT, | |
| color: `hsl(${Math.random() * 360}, 90%, 60%)`, // More saturated colors | |
| value: 1, | |
| size: FOOD_SIZE | |
| }); | |
| } | |
| // Generate AI tanks with distinct colors and positions | |
| for (let i = 0; i < MAX_AI; i++) { | |
| const aiId = 'ai_' + i; | |
| gameState.players[aiId] = { | |
| x: Math.random() * CANVAS_WIDTH, | |
| y: Math.random() * CANVAS_HEIGHT, | |
| angle: Math.random() * Math.PI * 2, | |
| health: 100, | |
| maxHealth: 100, | |
| name: 'AI-' + (i+1), | |
| color: `hsl(${i * 36}, 90%, 50%)`, // Distinct colors for each AI | |
| score: Math.floor(Math.random() * 500) | |
| }; | |
| } | |
| // Start game loop | |
| gameLoop(); | |
| // Connect to server (simulated) | |
| // Create player immediately | |
| gameState.playerId = 'player_' + Math.random().toString(36).substr(2, 9); | |
| gameState.players[gameState.playerId] = { | |
| x: CANVAS_WIDTH / 2, | |
| y: CANVAS_HEIGHT / 2, | |
| angle: 0, | |
| health: 100, | |
| maxHealth: 100, | |
| name: 'Player' + Math.floor(Math.random() * 1000), | |
| color: '#4F46E5', // Consistent blue color for player | |
| score: 0 | |
| }; | |
| // Show upgrade menu when player reaches level 2 | |
| if (gameState.level >= 2) { | |
| document.getElementById('upgradeMenu').classList.remove('hidden'); | |
| } | |
| }, 1000); | |
| } | |
| // Event listeners | |
| window.addEventListener('resize', () => { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| viewport.width = canvas.width; | |
| viewport.height = canvas.height; | |
| }); | |
| // Keyboard controls | |
| const keys = {}; | |
| const movementKeys = ['w', 'a', 's', 'd', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']; | |
| window.addEventListener('keydown', (e) => { | |
| keys[e.key] = true; | |
| // Space to shoot | |
| if (e.key === ' ' && gameState.playerId) { | |
| shootBullet(); | |
| } | |
| // H to toggle help | |
| if (e.key.toLowerCase() === 'h') { | |
| const helpMenu = document.getElementById('helpMenu'); | |
| if (helpMenu.classList.contains('hidden')) { | |
| helpMenu.classList.remove('hidden'); | |
| } else { | |
| helpMenu.classList.add('hidden'); | |
| } | |
| } | |
| // Movement handling | |
| if (gameState.playerId && movementKeys.includes(e.key.toLowerCase())) { | |
| handleMovement(); | |
| } | |
| }); | |
| function handleMovement() { | |
| if (!gameState.playerId) return; | |
| const player = gameState.players[gameState.playerId]; | |
| const moveSpeed = 5 + gameState.upgrades.movement * 0.5; | |
| if (keys['w'] || keys['ArrowUp']) player.y -= moveSpeed; | |
| if (keys['s'] || keys['ArrowDown']) player.y += moveSpeed; | |
| if (keys['a'] || keys['ArrowLeft']) player.x -= moveSpeed; | |
| if (keys['d'] || keys['ArrowRight']) player.x += moveSpeed; | |
| // Boundary checks | |
| player.x = Math.max(PLAYER_SIZE/2, Math.min(CANVAS_WIDTH - PLAYER_SIZE/2, player.x)); | |
| player.y = Math.max(PLAYER_SIZE/2, Math.min(CANVAS_HEIGHT - PLAYER_SIZE/2, player.y)); | |
| } | |
| window.addEventListener('keyup', (e) => { | |
| keys[e.key] = false; | |
| }); | |
| // Game loop update to handle continuous movement | |
| function update() { | |
| // Update viewport to follow player | |
| if (gameState.playerId && gameState.players[gameState.playerId]) { | |
| handleMovement(); | |
| const player = gameState.players[gameState.playerId]; | |
| viewport.target.x = player.x - viewport.width / 2; | |
| viewport.target.y = player.y - viewport.height / 2; | |
| // Smooth viewport movement | |
| viewport.x += (viewport.target.x - viewport.x) * 0.1; | |
| viewport.y += (viewport.target.y - viewport.y) * 0.1; | |
| } | |
| } | |
| function shootBullet() { | |
| if (!gameState.playerId) return; | |
| const player = gameState.players[gameState.playerId]; | |
| const bulletSpeed = 10 + gameState.upgrades.bulletSpeed * 2; | |
| gameState.bullets.push({ | |
| x: player.x + Math.cos(player.angle) * (PLAYER_SIZE/2 + 20), | |
| y: player.y + Math.sin(player.angle) * (PLAYER_SIZE/2 + 20), | |
| dx: Math.cos(player.angle) * bulletSpeed, | |
| dy: Math.sin(player.angle) * bulletSpeed, | |
| color: player.color, | |
| damage: 10 + gameState.upgrades.damage * 2, | |
| owner: gameState.playerId, | |
| penetration: 1 + gameState.upgrades.penetration | |
| }); | |
| } | |
| // Mouse controls | |
| canvas.addEventListener('mousemove', (e) => { | |
| if (!gameState.playerId) return; | |
| const player = gameState.players[gameState.playerId]; | |
| const rect = canvas.getBoundingClientRect(); | |
| const mouseX = e.clientX - rect.left + viewport.x; | |
| const mouseY = e.clientY - rect.top + viewport.y; | |
| player.angle = Math.atan2(mouseY - player.y, mouseX - player.x); | |
| }); | |
| canvas.addEventListener('click', (e) => { | |
| if (gameState.playerId) { | |
| shootBullet(); | |
| } | |
| }); | |
| // Upgrade buttons | |
| document.querySelectorAll('.upgrade-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const stat = btn.dataset.stat; | |
| if (gameState.upgradePoints > 0) { | |
| gameState.upgrades[stat]++; | |
| gameState.upgradePoints--; | |
| updatePlayerStats(); | |
| } | |
| }); | |
| }); | |
| function updatePlayerStats() { | |
| if (!gameState.playerId) return; | |
| const player = gameState.players[gameState.playerId]; | |
| player.maxHealth = 100 + gameState.upgrades.health * 20; | |
| // Apply other stat upgrades... | |
| } | |
| // Show initial help for 5 seconds then auto-hide | |
| setTimeout(() => { | |
| document.getElementById('helpMenu').classList.add('hidden'); | |
| }, 5000); | |
| // Start the game | |
| initGame(); | |
| // Simulate multiplayer updates | |
| setInterval(() => { | |
| if (!gameState.playerId) return; | |
| // Simulate other players moving and shooting | |
| for (const id in gameState.players) { | |
| if (id !== gameState.playerId) { | |
| const player = gameState.players[id]; | |
| const targetPlayer = gameState.players[gameState.playerId]; | |
| if (!targetPlayer) continue; | |
| // Move toward player with some randomness | |
| const angleToPlayer = Math.atan2( | |
| targetPlayer.y - player.y, | |
| targetPlayer.x - player.x | |
| ); | |
| const moveSpeed = 2; | |
| player.x += Math.cos(angleToPlayer) * moveSpeed + (Math.random() - 0.5); | |
| player.y += Math.sin(angleToPlayer) * moveSpeed + (Math.random() - 0.5); | |
| player.angle = angleToPlayer; | |
| // Shoot more frequently when close | |
| const distance = Math.sqrt( | |
| Math.pow(targetPlayer.x - player.x, 2) + | |
| Math.pow(targetPlayer.y - player.y, 2) | |
| ); | |
| if (distance < 300 && Math.random() < 0.05) { | |
| shootBullet(id); | |
| } | |
| } | |
| } | |
| // Update bullets | |
| for (let i = gameState.bullets.length - 1; i >= 0; i--) { | |
| const bullet = gameState.bullets[i]; | |
| bullet.x += bullet.dx; | |
| bullet.y += bullet.dy; | |
| // Remove bullets that go out of bounds | |
| if (bullet.x < 0 || bullet.x > CANVAS_WIDTH || | |
| bullet.y < 0 || bullet.y > CANVAS_HEIGHT) { | |
| gameState.bullets.splice(i, 1); | |
| continue; | |
| } | |
| // Check for collisions with players | |
| for (const id in gameState.players) { | |
| const player = gameState.players[id]; | |
| if (id !== bullet.owner) { | |
| const dx = player.x - bullet.x; | |
| const dy = player.y - bullet.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < PLAYER_SIZE/2 + BULLET_SIZE) { | |
| player.health -= bullet.damage; | |
| bullet.penetration--; | |
| if (bullet.penetration <= 0) { | |
| gameState.bullets.splice(i, 1); | |
| } | |
| if (player.health <= 0) { | |
| // Player died | |
| if (bullet.owner === gameState.playerId) { | |
| gameState.score += 100; | |
| checkLevelUp(); | |
| } | |
| delete gameState.players[id]; | |
| } | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| // Check for food collection | |
| if (gameState.playerId) { | |
| const player = gameState.players[gameState.playerId]; | |
| for (let i = gameState.food.length - 1; i >= 0; i--) { | |
| const food = gameState.food[i]; | |
| const dx = player.x - food.x; | |
| const dy = player.y - food.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < PLAYER_SIZE/2 + FOOD_SIZE) { | |
| gameState.score += food.value; | |
| gameState.food.splice(i, 1); | |
| checkLevelUp(); | |
| // Add new food to maintain count | |
| gameState.food.push({ | |
| x: Math.random() * CANVAS_WIDTH, | |
| y: Math.random() * CANVAS_HEIGHT, | |
| color: `hsl(${Math.random() * 360}, 70%, 60%)`, | |
| value: 1 | |
| }); | |
| } | |
| } | |
| } | |
| // Update leaderboard | |
| updateLeaderboard(); | |
| }, 1000 / 60); | |
| function checkLevelUp() { | |
| const needed = gameState.level * 1000; | |
| if (gameState.score >= needed) { | |
| gameState.level++; | |
| gameState.upgradePoints++; | |
| if (gameState.level >= 2) { | |
| document.getElementById('upgradeMenu').classList.remove('hidden'); | |
| } | |
| } | |
| } | |
| function updateLeaderboard() { | |
| const leaderboardList = document.getElementById('leaderboardList'); | |
| leaderboardList.innerHTML = ''; | |
| // Sort players by score | |
| const players = Object.values(gameState.players); | |
| players.sort((a, b) => (b.score || 0) - (a.score || 0)); | |
| // Display top 10 | |
| players.slice(0, 10).forEach(player => { | |
| const li = document.createElement('li'); | |
| li.className = 'flex justify-between'; | |
| li.innerHTML = ` | |
| <span class="player-name">${player.name}</span> | |
| <span class="player-score">${player.score || 0}</span> | |
| `; | |
| leaderboardList.appendChild(li); | |
| }); | |
| } | |
| // Simulated shoot function for other players | |
| function shootBullet(playerId) { | |
| const player = gameState.players[playerId]; | |
| if (!player) return; | |
| const isPlayer = playerId === gameState.playerId; | |
| const baseSpeed = isPlayer ? 10 + gameState.upgrades.bulletSpeed * 2 : 8; | |
| const baseDamage = isPlayer ? 10 + gameState.upgrades.damage * 2 : 8; | |
| gameState.bullets.push({ | |
| x: player.x + Math.cos(player.angle) * (PLAYER_SIZE/2 + 20), | |
| y: player.y + Math.sin(player.angle) * (PLAYER_SIZE/2 + 20), | |
| dx: Math.cos(player.angle) * baseSpeed, | |
| dy: Math.sin(player.angle) * baseSpeed, | |
| color: player.color, | |
| damage: baseDamage, | |
| owner: playerId, | |
| penetration: isPlayer ? 1 + gameState.upgrades.penetration : 1 | |
| }); | |
| } | |
| // Initialize with more visible AI tanks | |
| function initGame() { | |
| // Generate initial food | |
| for (let i = 0; i < MAX_FOOD; i++) { | |
| gameState.food.push({ | |
| x: Math.random() * CANVAS_WIDTH, | |
| y: Math.random() * CANVAS_HEIGHT, | |
| color: '#00FF00', | |
| value: 1 | |
| }); | |
| } | |
| // Generate AI tanks with distinct colors | |
| for (let i = 0; i < MAX_AI; i++) { | |
| const aiId = 'ai_' + i; | |
| gameState.players[aiId] = { | |
| x: Math.random() * CANVAS_WIDTH, | |
| y: Math.random() * CANVAS_HEIGHT, | |
| angle: Math.random() * Math.PI * 2, | |
| health: 100, | |
| maxHealth: 100, | |
| name: 'AI-' + i, | |
| color: `hsl(${i * 36}, 80%, 50%)`, | |
| score: Math.floor(Math.random() * 500) | |
| }; | |
| } | |