Spaces:
Running
Running
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const scoreElement = document.getElementById('score'); | |
| const livesElement = document.getElementById('lives'); | |
| // Game elements | |
| const player = { | |
| x: canvas.width / 2, | |
| y: canvas.height / 2, | |
| radius: 15, | |
| color: '#FF5733', | |
| speed: 5 | |
| }; | |
| const enemy = { | |
| x: 50, | |
| y: 50, | |
| radius: 18, | |
| color: '#DC143C', | |
| speed: 2.5, | |
| stolenScore: 0, | |
| alive: true, | |
| respawnTimer: 0 | |
| }; | |
| let angerMode = false; | |
| let angerCooldown = 0; | |
| let playerMoveCount = 0; | |
| let lastPlayerX = null; | |
| let lastPlayerY = null; | |
| const toppings = { | |
| beef: [], | |
| lettuce: [], | |
| cheese: [] | |
| }; | |
| let score = 0; | |
| let lives = 3; | |
| let gameRunning = true; | |
| const TOTAL_POINTS = 200; // 20 beef * 10 points each | |
| // Initialize game | |
| function init() { | |
| // Create toppings | |
| for (let i = 0; i < 20; i++) { | |
| toppings.beef.push(createTopping('beef')); | |
| toppings.lettuce.push(createTopping('lettuce')); | |
| if (i < 5) toppings.cheese.push(createTopping('cheese')); | |
| } | |
| // Event listeners | |
| window.addEventListener('keydown', movePlayer); | |
| // Start game loop | |
| gameLoop(); | |
| } | |
| function createTopping(type) { | |
| return { | |
| x: Math.random() * (canvas.width - 30) + 15, | |
| y: Math.random() * (canvas.height - 30) + 15, | |
| radius: 10, | |
| type: type, | |
| color: type === 'beef' ? '#8B4513' : | |
| type === 'lettuce' ? '#7CFC00' : '#FFD700' | |
| }; | |
| } | |
| function movePlayer(e) { | |
| if (!gameRunning) { | |
| // Allow reset with R or Space when game is over | |
| if (e.key === 'r' || e.key === 'R' || e.key === ' ') { | |
| resetGame(); | |
| } | |
| return; | |
| } | |
| // Activate anger mode with Shift | |
| if (e.key === 'Shift' && angerCooldown <= 0 && !angerMode) { | |
| angerMode = true; | |
| angerCooldown = 300; // 5 seconds at 60fps | |
| setTimeout(() => { angerMode = false; }, 2000); // Anger lasts 2 seconds | |
| } | |
| // Store position before move | |
| lastPlayerX = player.x; | |
| lastPlayerY = player.y; | |
| switch(e.key) { | |
| case 'ArrowUp': player.y -= player.speed; break; | |
| case 'ArrowDown': player.y += player.speed; break; | |
| case 'ArrowLeft': player.x -= player.speed; break; | |
| case 'ArrowRight': player.x += player.speed; break; | |
| } | |
| // Boundary check | |
| player.x = Math.max(player.radius, Math.min(canvas.width - player.radius, player.x)); | |
| player.y = Math.max(player.radius, Math.min(canvas.height - player.radius, player.y)); | |
| // Only count as a move if position actually changed | |
| if (player.x !== lastPlayerX || player.y !== lastPlayerY) { | |
| playerMoveCount++; | |
| } | |
| } | |
| function resetGame() { | |
| // Reset player | |
| player.x = canvas.width / 2; | |
| player.y = canvas.height / 2; | |
| // Reset enemy | |
| enemy.x = 50; | |
| enemy.y = 50; | |
| enemy.stolenScore = 0; | |
| enemy.alive = true; | |
| enemy.respawnTimer = 0; | |
| // Reset anger mode | |
| angerMode = false; | |
| angerCooldown = 0; | |
| // Reset game state | |
| score = 0; | |
| lives = 3; | |
| playerMoveCount = 0; | |
| lastPlayerX = null; | |
| lastPlayerY = null; | |
| gameRunning = true; | |
| // Reset toppings | |
| toppings.beef = []; | |
| toppings.lettuce = []; | |
| toppings.cheese = []; | |
| // Recreate toppings | |
| for (let i = 0; i < 20; i++) { | |
| toppings.beef.push(createTopping('beef')); | |
| toppings.lettuce.push(createTopping('lettuce')); | |
| if (i < 5) toppings.cheese.push(createTopping('cheese')); | |
| } | |
| // Update display | |
| scoreElement.textContent = score; | |
| livesElement.textContent = lives; | |
| // Restart loop | |
| gameLoop(); | |
| } | |
| function moveEnemy() { | |
| if (!gameRunning || !enemy.alive) { | |
| // Handle respawn | |
| if (!enemy.alive) { | |
| enemy.respawnTimer--; | |
| if (enemy.respawnTimer <= 0) { | |
| enemy.alive = true; | |
| enemy.x = 50; | |
| enemy.y = 50; | |
| } | |
| } | |
| return; | |
| } | |
| // Enemy only moves once for every 2 player moves | |
| if (playerMoveCount % 2 !== 0) return; | |
| // Calculate direction to player | |
| const dx = player.x - enemy.x; | |
| const dy = player.y - enemy.y; | |
| const distance = Math.hypot(dx, dy); | |
| if (distance > 0) { | |
| // Normalize and move towards player | |
| enemy.x += (dx / distance) * enemy.speed; | |
| enemy.y += (dy / distance) * enemy.speed; | |
| } | |
| // Boundary check | |
| enemy.x = Math.max(enemy.radius, Math.min(canvas.width - enemy.radius, enemy.x)); | |
| enemy.y = Math.max(enemy.radius, Math.min(canvas.height - enemy.radius, enemy.y)); | |
| } | |
| function checkCollision() { | |
| // Check beef collisions | |
| for (let i = 0; i < toppings.beef.length; i++) { | |
| const beef = toppings.beef[i]; | |
| const dist = Math.hypot(player.x - beef.x, player.y - beef.y); | |
| if (dist < player.radius + beef.radius) { | |
| toppings.beef.splice(i, 1); | |
| score += 10; | |
| scoreElement.textContent = score; | |
| break; | |
| } | |
| } | |
| // Check lettuce collisions | |
| for (let i = 0; i < toppings.lettuce.length; i++) { | |
| const lettuce = toppings.lettuce[i]; | |
| const dist = Math.hypot(player.x - lettuce.x, player.y - lettuce.y); | |
| if (dist < player.radius + lettuce.radius) { | |
| toppings.lettuce.splice(i, 1); | |
| score = Math.max(0, score - 5); | |
| scoreElement.textContent = score; | |
| break; | |
| } | |
| } | |
| // Check cheese collisions | |
| for (let i = 0; i < toppings.cheese.length; i++) { | |
| const cheese = toppings.cheese[i]; | |
| const dist = Math.hypot(player.x - cheese.x, player.y - cheese.y); | |
| if (dist < player.radius + cheese.radius) { | |
| toppings.cheese.splice(i, 1); | |
| lives++; | |
| livesElement.textContent = lives; | |
| break; | |
| } | |
| } | |
| // Check enemy collision with player | |
| if (enemy.alive) { | |
| const enemyDist = Math.hypot(player.x - enemy.x, player.y - enemy.y); | |
| if (enemyDist < player.radius + enemy.radius) { | |
| if (angerMode) { | |
| // Player kills enemy with anger! | |
| enemy.alive = false; | |
| enemy.respawnTimer = 180; // Respawn after 3 seconds | |
| score += 25; // Bonus for killing enemy | |
| scoreElement.textContent = score; | |
| // Visual feedback | |
| ctx.fillStyle = 'white'; | |
| ctx.font = 'bold 30px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText('💥 RAGE KILL! 💥', canvas.width/2, canvas.height/2); | |
| } else { | |
| // Enemy steals points from player | |
| if (score > 0) { | |
| const stealAmount = Math.min(10, score); // Steal up to 10 points at a time | |
| score -= stealAmount; | |
| enemy.stolenScore += stealAmount; | |
| scoreElement.textContent = score; | |
| // Push player away from enemy | |
| const pushAngle = Math.atan2(player.y - enemy.y, player.x - enemy.x); | |
| player.x += Math.cos(pushAngle) * 30; | |
| player.y += Math.sin(pushAngle) * 30; | |
| // Boundary check after push | |
| player.x = Math.max(player.radius, Math.min(canvas.width - player.radius, player.x)); | |
| player.y = Math.max(player.radius, Math.min(canvas.height - player.radius, player.y)); | |
| } | |
| } | |
| } | |
| } | |
| // Check win/lose conditions | |
| if (toppings.beef.length === 0) { | |
| gameRunning = false; | |
| alert('You won! Final score: ' + score + '\nEnemy stole: ' + enemy.stolenScore + ' points\nPress R or Space to restart'); | |
| } else if (enemy.stolenScore >= TOTAL_POINTS) { | |
| gameRunning = false; | |
| alert('The Red Devourer wins!\nIt stole all ' + enemy.stolenScore + ' points from you!\nYour final score: ' + score + '\nPress R or Space to restart'); | |
| } else if (lives <= 0) { | |
| gameRunning = false; | |
| alert('Game over! Final score: ' + score + '\nPress R or Space to restart'); | |
| } | |
| } | |
| function draw() { | |
| // Clear canvas | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Draw player (glows red when angry) | |
| ctx.beginPath(); | |
| ctx.arc(player.x, player.y, player.radius, 0, Math.PI * 2); | |
| if (angerMode) { | |
| // Pulsing red glow | |
| const pulse = Math.sin(Date.now() / 100) * 5 + 10; | |
| ctx.shadowBlur = 20; | |
| ctx.shadowColor = '#FF0000'; | |
| ctx.fillStyle = '#FF0000'; | |
| } else { | |
| ctx.shadowBlur = 0; | |
| ctx.fillStyle = player.color; | |
| } | |
| ctx.fill(); | |
| ctx.shadowBlur = 0; // Reset shadow | |
| // Draw anger meter/cooldown | |
| if (angerCooldown > 0) { | |
| angerCooldown--; | |
| ctx.fillStyle = 'rgba(255, 0, 0, 0.3)'; | |
| ctx.fillRect(10, 70, 200, 20); | |
| ctx.fillStyle = angerCooldown > 0 ? '#FF0000' : '#00FF00'; | |
| const cooldownWidth = (1 - angerCooldown / 300) * 200; | |
| ctx.fillRect(10, 70, cooldownWidth, 20); | |
| ctx.strokeStyle = '#8B4513'; | |
| ctx.lineWidth = 2; | |
| ctx.strokeRect(10, 70, 200, 20); | |
| ctx.fillStyle = '#8B4513'; | |
| ctx.font = 'bold 14px Arial'; | |
| ctx.textAlign = 'left'; | |
| ctx.fillText(angerMode ? '🔥 RAGE ACTIVE! 🔥' : (angerCooldown <= 0 ? 'SHIFT: ACTIVATE RAGE' : 'RAGE COOLDOWN'), 15, 85); | |
| } else { | |
| ctx.fillStyle = '#00AA00'; | |
| ctx.font = 'bold 14px Arial'; | |
| ctx.textAlign = 'left'; | |
| ctx.fillText('✓ SHIFT: ACTIVATE RAGE MODE', 15, 85); | |
| } | |
| // Draw enemy if alive | |
| if (enemy.alive) { | |
| ctx.beginPath(); | |
| ctx.arc(enemy.x, enemy.y, enemy.radius, 0, Math.PI * 2); | |
| ctx.fillStyle = angerMode ? '#FF0000' : enemy.color; | |
| ctx.fill(); | |
| // Draw enemy eyes (angry when player is in anger mode) | |
| ctx.fillStyle = angerMode ? '#FF0000' : 'white'; | |
| ctx.beginPath(); | |
| ctx.arc(enemy.x - 6, enemy.y - 4, 4, 0, Math.PI * 2); | |
| ctx.arc(enemy.x + 6, enemy.y - 4, 4, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.fillStyle = angerMode ? '#000' : 'black'; | |
| ctx.beginPath(); | |
| ctx.arc(enemy.x - 6, enemy.y - 4, 2, 0, Math.PI * 2); | |
| ctx.arc(enemy.x + 6, enemy.y - 4, 2, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Draw scared eyebrows when player is angry | |
| if (angerMode) { | |
| ctx.strokeStyle = '#000'; | |
| ctx.lineWidth = 2; | |
| ctx.beginPath(); | |
| ctx.moveTo(enemy.x - 10, enemy.y - 8); | |
| ctx.lineTo(enemy.x - 2, enemy.y - 6); | |
| ctx.moveTo(enemy.x + 10, enemy.y - 8); | |
| ctx.lineTo(enemy.x + 2, enemy.y - 6); | |
| ctx.stroke(); | |
| } | |
| } else { | |
| // Draw skull when enemy is dead | |
| ctx.fillStyle = 'rgba(100, 100, 100, 0.5)'; | |
| ctx.font = '30px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText('💀', enemy.x, enemy.y + 10); | |
| // Respawn countdown | |
| ctx.fillStyle = '#DC143C'; | |
| ctx.font = 'bold 16px Arial'; | |
| ctx.fillText('RESPAWN: ' + Math.ceil(enemy.respawnTimer/60), enemy.x, enemy.y - 25); | |
| } | |
| // Draw toppings | |
| drawToppings(toppings.beef); | |
| drawToppings(toppings.lettuce); | |
| drawToppings(toppings.cheese); | |
| // Draw enemy threat meter | |
| ctx.fillStyle = 'rgba(220, 20, 60, 0.3)'; | |
| ctx.fillRect(canvas.width - 210, 10, 200, 20); | |
| ctx.fillStyle = '#DC143C'; | |
| const threatWidth = (enemy.stolenScore / TOTAL_POINTS) * 200; | |
| ctx.fillRect(canvas.width - 210, 10, threatWidth, 20); | |
| ctx.strokeStyle = '#8B4513'; | |
| ctx.lineWidth = 2; | |
| ctx.strokeRect(canvas.width - 210, 10, 200, 20); | |
| ctx.fillStyle = '#8B4513'; | |
| ctx.font = 'bold 14px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText('ENEMY THREAT: ' + enemy.stolenScore + '/' + TOTAL_POINTS, canvas.width - 110, 25); | |
| } | |
| function drawToppings(toppingArray) { | |
| toppingArray.forEach(topping => { | |
| ctx.beginPath(); | |
| ctx.arc(topping.x, topping.y, topping.radius, 0, Math.PI * 2); | |
| ctx.fillStyle = topping.color; | |
| ctx.fill(); | |
| }); | |
| } | |
| function gameLoop() { | |
| if (!gameRunning) return; | |
| moveEnemy(); | |
| draw(); | |
| checkCollision(); | |
| requestAnimationFrame(gameLoop); | |
| } | |
| // Start the game | |
| init(); |