Spaces:
Running
Running
| // Game initialization | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Initialize game | |
| const game = new WormateGame(); | |
| game.init(); | |
| // Initialize Feather Icons | |
| if (typeof feather !== 'undefined') { | |
| feather.replace(); | |
| } | |
| }); | |
| class WormateGame { | |
| constructor() { | |
| this.canvas = document.getElementById('gameCanvas'); | |
| this.ctx = this.canvas.getContext('2d'); | |
| this.scoreDisplay = document.getElementById('score-display'); | |
| this.leaderboard = document.getElementById('leaderboard'); | |
| this.playBtn = document.getElementById('play-btn'); | |
| this.timeDisplay = document.getElementById('time-display'); | |
| this.gameStarted = false; | |
| this.playerId = null; | |
| this.players = {}; | |
| this.foods = []; | |
| this.powerUps = []; | |
| this.score = 0; | |
| this.timeLeft = 60; // Starting time (seconds) | |
| this.maxTime = 120; // Maximum time bank | |
| this.timeDecayRate = 1.0; // Base decay rate (seconds per second) | |
| this.timeDecayMultiplier = 0.01; // Increases as worm grows | |
| this.gameTimer = null; | |
| this.keys = { | |
| ArrowUp: false, | |
| ArrowDown: false, | |
| ArrowLeft: false, | |
| ArrowRight: false | |
| }; | |
| } | |
| init() { | |
| this.resizeCanvas(); | |
| window.addEventListener('resize', () => this.resizeCanvas()); | |
| // Keyboard controls | |
| window.addEventListener('keydown', (e) => { | |
| if (this.keys.hasOwnProperty(e.key)) { | |
| this.keys[e.key] = true; | |
| e.preventDefault(); | |
| } | |
| }); | |
| window.addEventListener('keyup', (e) => { | |
| if (this.keys.hasOwnProperty(e.key)) { | |
| this.keys[e.key] = false; | |
| e.preventDefault(); | |
| } | |
| }); | |
| // Start game button | |
| this.playBtn.addEventListener('click', () => this.startGame()); | |
| // Start game loop | |
| this.gameLoop(); | |
| } | |
| resizeCanvas() { | |
| this.canvas.width = window.innerWidth; | |
| this.canvas.height = window.innerHeight; | |
| } | |
| startGame() { | |
| const playerName = prompt('Enter your name:', 'Player' + Math.floor(Math.random() * 1000)); | |
| if (playerName) { | |
| this.playerId = 'local-' + Date.now(); | |
| this.players[this.playerId] = { | |
| id: this.playerId, | |
| name: playerName, | |
| color: this.getRandomColor(), | |
| score: 0, | |
| segments: [ | |
| { x: this.canvas.width/2, y: this.canvas.height/2, radius: 15 }, | |
| { x: this.canvas.width/2 - 20, y: this.canvas.height/2, radius: 14 }, | |
| { x: this.canvas.width/2 - 40, y: this.canvas.height/2, radius: 13 } | |
| ] | |
| }; | |
| // Generate initial food | |
| for (let i = 0; i < 50; i++) { | |
| const foodTypes = [ | |
| { type: 'lollipop', radius: 12, color: '#FF5252', time: 2, rarity: 0.5 }, | |
| { type: 'gummybear', radius: 10, color: '#FF4081', time: 2, rarity: 0.5 }, | |
| { type: 'candycorn', radius: 8, color: '#FFD740', time: 2, rarity: 0.5 }, | |
| { type: 'chocolate', radius: 14, color: '#7B3F00', time: 2, rarity: 0.1 }, | |
| { type: 'marshmallow', radius: 9, color: '#FFFFFF', time: 2, rarity: 0.2 } | |
| ]; | |
| const randomFood = foodTypes[Math.floor(Math.random() * foodTypes.length)]; | |
| this.foods.push({ | |
| x: Math.random() * this.canvas.width, | |
| y: Math.random() * this.canvas.height, | |
| radius: randomFood.radius, | |
| color: randomFood.color, | |
| type: randomFood.type, | |
| points: randomFood.points, | |
| time: 2 // Fixed 2 seconds for all food types | |
| , | |
| time: 2 // Fixed 2 seconds for all food types | |
| }); | |
| } | |
| this.gameStarted = true; | |
| this.playBtn.style.display = 'none'; | |
| this.timeLeft = 30; // Reset time | |
| this.timeDisplay.textContent = `Time: ${this.timeLeft}s`; | |
| this.startTimer(); | |
| } | |
| } | |
| gameLoop() { | |
| if (this.gameStarted) { | |
| this.update(); | |
| this.render(); | |
| } | |
| requestAnimationFrame(() => this.gameLoop()); | |
| } | |
| startTimer() { | |
| clearInterval(this.gameTimer); | |
| let lastTime = Date.now(); | |
| this.gameTimer = setInterval(() => { | |
| const now = Date.now(); | |
| const delta = (now - lastTime) / 1000; // Convert to seconds | |
| lastTime = now; | |
| // Calculate time decay based on worm size | |
| const decayRate = this.timeDecayRate + | |
| (this.timeDecayMultiplier * this.players[this.playerId]?.segments.length || 0); | |
| this.timeLeft = Math.max(0, this.timeLeft - (decayRate * delta)); | |
| this.timeDisplay.textContent = `Time: ${Math.floor(this.timeLeft)}s`; | |
| if (this.timeLeft <= 0) { | |
| this.gameOver(); | |
| } | |
| // Visual feedback for time status | |
| if (this.timeLeft <= 10) { | |
| this.timeDisplay.classList.add('warning'); | |
| this.timeDisplay.style.animationDuration = `${0.5 / (11 - this.timeLeft)}s`; | |
| } else if (this.timeLeft <= 30) { | |
| this.timeDisplay.classList.add('warning'); | |
| this.timeDisplay.style.animationDuration = '1s'; | |
| } else { | |
| this.timeDisplay.classList.remove('warning'); | |
| } | |
| }, 1000); | |
| } | |
| update() { | |
| // Move player worm | |
| const player = this.players[this.playerId]; | |
| if (!player) return; | |
| const head = player.segments[0]; | |
| let newX = head.x; | |
| let newY = head.y; | |
| if (this.keys.ArrowUp) newY -= 5; | |
| if (this.keys.ArrowDown) newY += 5; | |
| if (this.keys.ArrowLeft) newX -= 5; | |
| if (this.keys.ArrowRight) newX += 5; | |
| // Add new head position | |
| player.segments.unshift({ | |
| x: newX, | |
| y: newY, | |
| radius: head.radius | |
| }); | |
| // Remove tail segment | |
| player.segments.pop(); | |
| // Check food collision | |
| this.foods = this.foods.filter(food => { | |
| const distance = Math.sqrt( | |
| Math.pow(head.x - food.x, 2) + | |
| Math.pow(head.y - food.y, 2) | |
| ); | |
| if (distance < head.radius + food.radius) { | |
| // Eat food - grow worm | |
| const tail = player.segments[player.segments.length - 1]; | |
| for (let i = 0; i < 5; i++) { | |
| player.segments.push({ | |
| x: tail.x, | |
| y: tail.y, | |
| radius: tail.radius * 0.95 | |
| }); | |
| } | |
| // Add time for eating food with diminishing returns based on worm size | |
| const timeGain = Math.max(1, food.time - (player.segments.length * 0.1)); | |
| this.timeLeft = Math.min(this.maxTime, this.timeLeft + timeGain); | |
| // Increase time decay as worm grows (harder to maintain) | |
| this.timeDecayMultiplier = 0.01 + (player.segments.length * 0.0005); | |
| player.score += 1; | |
| this.score = player.score; | |
| this.scoreDisplay.textContent = `Score: ${this.score}`; | |
| this.timeDisplay.textContent = `Time: ${this.timeLeft}s`; | |
| return false; // Remove food | |
| } | |
| return true; // Keep food | |
| }); | |
| // Add new food if needed | |
| if (this.foods.length < 30 && Math.random() < 0.1) { | |
| const foodTypes = [ | |
| { type: 'lollipop', radius: 12, color: '#FF5252', points: 10, time: 2 }, | |
| { type: 'gummybear', radius: 10, color: '#FF4081', points: 5, time: 2 }, | |
| { type: 'candycorn', radius: 8, color: '#FFD740', points: 3, time: 2 }, | |
| { type: 'chocolate', radius: 14, color: '#7B3F00', points: 15, time: 2 }, | |
| { type: 'marshmallow', radius: 9, color: '#FFFFFF', points: 8, time: 2 } | |
| ]; | |
| const randomFood = foodTypes[Math.floor(Math.random() * foodTypes.length)]; | |
| this.foods.push({ | |
| x: Math.random() * this.canvas.width, | |
| y: Math.random() * this.canvas.height, | |
| radius: randomFood.radius, | |
| color: randomFood.color, | |
| type: randomFood.type, | |
| points: randomFood.points | |
| }); | |
| } | |
| // Check wall collision | |
| if ( | |
| head.x < -head.radius || | |
| head.x > this.canvas.width + head.radius || | |
| head.y < -head.radius || | |
| head.y > this.canvas.height + head.radius | |
| ) { | |
| this.gameOver(); | |
| } | |
| // Update leaderboard | |
| this.updateLeaderboard(); | |
| } | |
| render() { | |
| // Clear canvas | |
| this.ctx.fillStyle = '#1a1a2e'; | |
| this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); | |
| // Draw foods | |
| this.foods.forEach(food => { | |
| this.ctx.save(); | |
| this.ctx.translate(food.x, food.y); | |
| switch(food.type) { | |
| case 'lollipop': | |
| // Lollipop head | |
| this.ctx.fillStyle = food.color; | |
| this.ctx.beginPath(); | |
| this.ctx.arc(0, 0, food.radius, 0, Math.PI * 2); | |
| this.ctx.fill(); | |
| // Lollipop stick | |
| this.ctx.strokeStyle = '#FFFFFF'; | |
| this.ctx.lineWidth = 2; | |
| this.ctx.beginPath(); | |
| this.ctx.moveTo(0, food.radius); | |
| this.ctx.lineTo(0, food.radius * 2); | |
| this.ctx.stroke(); | |
| break; | |
| case 'gummybear': | |
| // Gummy bear body | |
| this.ctx.fillStyle = food.color; | |
| this.ctx.beginPath(); | |
| this.ctx.ellipse(0, 0, food.radius, food.radius * 1.2, 0, 0, Math.PI * 2); | |
| this.ctx.fill(); | |
| // Eyes | |
| this.ctx.fillStyle = '#000000'; | |
| this.ctx.beginPath(); | |
| this.ctx.arc(-food.radius/3, -food.radius/3, 2, 0, Math.PI * 2); | |
| this.ctx.arc(food.radius/3, -food.radius/3, 2, 0, Math.PI * 2); | |
| this.ctx.fill(); | |
| break; | |
| case 'candycorn': | |
| // Candy corn triangle | |
| this.ctx.fillStyle = food.color; | |
| this.ctx.beginPath(); | |
| this.ctx.moveTo(0, -food.radius); | |
| this.ctx.lineTo(-food.radius, food.radius); | |
| this.ctx.lineTo(food.radius, food.radius); | |
| this.ctx.closePath(); | |
| this.ctx.fill(); | |
| break; | |
| case 'chocolate': | |
| // Chocolate bar | |
| this.ctx.fillStyle = food.color; | |
| this.ctx.fillRect(-food.radius, -food.radius/2, food.radius*2, food.radius); | |
| break; | |
| case 'marshmallow': | |
| // Marshmallow | |
| this.ctx.fillStyle = food.color; | |
| this.ctx.beginPath(); | |
| this.ctx.arc(0, 0, food.radius, 0, Math.PI * 2); | |
| this.ctx.fill(); | |
| // Shading | |
| this.ctx.fillStyle = 'rgba(0,0,0,0.1)'; | |
| this.ctx.beginPath(); | |
| this.ctx.arc(0, -food.radius/3, food.radius/3, 0, Math.PI * 2); | |
| this.ctx.fill(); | |
| break; | |
| } | |
| this.ctx.restore(); | |
| }); | |
| // Draw players | |
| Object.values(this.players).forEach(player => { | |
| // Draw worm segments | |
| player.segments.forEach((segment, i) => { | |
| const gradient = this.ctx.createRadialGradient( | |
| segment.x, segment.y, 0, | |
| segment.x, segment.y, segment.radius | |
| ); | |
| gradient.addColorStop(0, player.color); | |
| gradient.addColorStop(1, this.darkenColor(player.color, 0.3)); | |
| this.ctx.fillStyle = gradient; | |
| this.ctx.beginPath(); | |
| this.ctx.arc(segment.x, segment.y, segment.radius, 0, Math.PI * 2); | |
| this.ctx.fill(); | |
| // Draw eyes on head | |
| if (i === 0 && player.segments.length > 1) { | |
| const angle = Math.atan2( | |
| player.segments[1].y - segment.y, | |
| player.segments[1].x - segment.x | |
| ); | |
| const eyeRadius = segment.radius * 0.3; | |
| const eyeOffset = segment.radius * 0.6; | |
| // Eyes | |
| this.ctx.fillStyle = 'white'; | |
| this.ctx.beginPath(); | |
| this.ctx.arc( | |
| segment.x + Math.cos(angle + Math.PI/2) * eyeOffset, | |
| segment.y + Math.sin(angle + Math.PI/2) * eyeOffset, | |
| eyeRadius, 0, Math.PI * 2 | |
| ); | |
| this.ctx.fill(); | |
| this.ctx.beginPath(); | |
| this.ctx.arc( | |
| segment.x + Math.cos(angle - Math.PI/2) * eyeOffset, | |
| segment.y + Math.sin(angle - Math.PI/2) * eyeOffset, | |
| eyeRadius, 0, Math.PI * 2 | |
| ); | |
| this.ctx.fill(); | |
| // Pupils | |
| this.ctx.fillStyle = 'black'; | |
| this.ctx.beginPath(); | |
| this.ctx.arc( | |
| segment.x + Math.cos(angle + Math.PI/2) * eyeOffset + Math.cos(angle) * eyeRadius/2, | |
| segment.y + Math.sin(angle + Math.PI/2) * eyeOffset + Math.sin(angle) * eyeRadius/2, | |
| eyeRadius/2, 0, Math.PI * 2 | |
| ); | |
| this.ctx.fill(); | |
| this.ctx.beginPath(); | |
| this.ctx.arc( | |
| segment.x + Math.cos(angle - Math.PI/2) * eyeOffset + Math.cos(angle) * eyeRadius/2, | |
| segment.y + Math.sin(angle - Math.PI/2) * eyeOffset + Math.sin(angle) * eyeRadius/2, | |
| eyeRadius/2, 0, Math.PI * 2 | |
| ); | |
| this.ctx.fill(); | |
| } | |
| }); | |
| // Draw player name | |
| if (player.segments.length > 0) { | |
| const head = player.segments[0]; | |
| this.ctx.fillStyle = 'white'; | |
| this.ctx.font = '12px "Press Start 2P", cursive'; | |
| this.ctx.textAlign = 'center'; | |
| this.ctx.fillText(player.name, head.x, head.y - head.radius - 10); | |
| } | |
| }); | |
| } | |
| updateLeaderboard() { | |
| const sortedPlayers = Object.values(this.players).sort((a, b) => b.score - a.score); | |
| this.leaderboard.innerHTML = '<h3>Leaderboard</h3>' + | |
| sortedPlayers.map(p => | |
| `<div class="player-row ${p.id === this.playerId ? 'you' : ''}"> | |
| <span class="player-name">${p.name}</span> | |
| <span class="player-score">${p.score}</span> | |
| </div>` | |
| ).join(''); | |
| } | |
| gameOver() { | |
| alert(`Game Over! Your score: ${this.score}`); | |
| this.gameStarted = false; | |
| this.playBtn.style.display = 'block'; | |
| this.players = {}; | |
| this.foods = []; | |
| this.score = 0; | |
| this.scoreDisplay.textContent = `Score: 0`; | |
| } | |
| getRandomColor() { | |
| const colors = [ | |
| '#FF5252', '#FF4081', '#E040FB', '#7C4DFF', | |
| '#536DFE', '#448AFF', '#40C4FF', '#18FFFF', | |
| '#64FFDA', '#69F0AE', '#B2FF59', '#EEFF41', | |
| '#FFFF00', '#FFD740', '#FFAB40', '#FF6E40' | |
| ]; | |
| return colors[Math.floor(Math.random() * colors.length)]; | |
| } | |
| darkenColor(color, amount) { | |
| let r = parseInt(color.substr(1, 2), 16); | |
| let g = parseInt(color.substr(3, 2), 16); | |
| let b = parseInt(color.substr(5, 2), 16); | |
| r = Math.max(0, Math.floor(r * (1 - amount))); | |
| g = Math.max(0, Math.floor(g * (1 - amount))); | |
| b = Math.max(0, Math.floor(b * (1 - amount))); | |
| return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; | |
| } | |
| } | |