Spaces:
Running
Running
can you make it so the map is larger and the camera follows you, and there are other "players" but they are ai, and if you are close and bigger, they will try and chase you. also add the jiggle when ever you get a player or blob it jiggles from where you got the points - Follow Up Deployment
b10e9f7
verified
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Blob.io - Single Player Agar.io Clone</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <style> | |
| body { | |
| margin: 0; | |
| overflow: hidden; | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| } | |
| canvas { | |
| display: block; | |
| background-color: #f0f9ff; | |
| } | |
| #gameUI { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| pointer-events: none; | |
| } | |
| #scoreContainer { | |
| background-color: rgba(255, 255, 255, 0.8); | |
| border-radius: 9999px; | |
| padding: 0.5rem 1rem; | |
| margin: 1rem; | |
| display: inline-flex; | |
| align-items: center; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
| } | |
| #startScreen, #gameOverScreen { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| background-color: rgba(0, 0, 0, 0.5); | |
| color: white; | |
| } | |
| .btn { | |
| pointer-events: auto; | |
| background-color: #3b82f6; | |
| color: white; | |
| border: none; | |
| padding: 0.75rem 1.5rem; | |
| border-radius: 0.375rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
| } | |
| .btn:hover { | |
| background-color: #2563eb; | |
| transform: translateY(-2px); | |
| } | |
| .btn:active { | |
| transform: translateY(0); | |
| } | |
| .hidden { | |
| display: none ; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <canvas id="gameCanvas"></canvas> | |
| <div id="gameUI"> | |
| <div id="scoreContainer" class="flex items-center"> | |
| <i data-feather="award" class="mr-2"></i> | |
| <span id="score">0</span> | |
| </div> | |
| </div> | |
| <div id="startScreen"> | |
| <h1 class="text-4xl font-bold mb-6">Blob.io</h1> | |
| <p class="text-xl mb-8 text-center max-w-md px-4">Eat smaller blobs to grow bigger. Avoid larger blobs!</p> | |
| <button id="startBtn" class="btn">Start Game</button> | |
| </div> | |
| <div id="gameOverScreen" class="hidden"> | |
| <h1 class="text-4xl font-bold mb-4">Game Over</h1> | |
| <p class="text-xl mb-2">Your score: <span id="finalScore">0</span></p> | |
| <p class="text-lg mb-8">High score: <span id="highScore">0</span></p> | |
| <button id="restartBtn" class="btn">Play Again</button> | |
| </div> | |
| <script> | |
| feather.replace(); | |
| // Game variables | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const scoreElement = document.getElementById('score'); | |
| // World dimensions (larger than canvas) | |
| const worldWidth = 5000; | |
| const worldHeight = 5000; | |
| let cameraOffset = { x: 0, y: 0 }; | |
| // Jiggle effect variables | |
| let jiggleOffset = { x: 0, y: 0 }; | |
| let jiggleTime = 0; | |
| const jiggleDuration = 0.3; | |
| const finalScoreElement = document.getElementById('finalScore'); | |
| const highScoreElement = document.getElementById('highScore'); | |
| const startScreen = document.getElementById('startScreen'); | |
| const gameOverScreen = document.getElementById('gameOverScreen'); | |
| const startBtn = document.getElementById('startBtn'); | |
| const restartBtn = document.getElementById('restartBtn'); | |
| // Set canvas size | |
| function resizeCanvas() { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| // Center camera on player initially | |
| cameraOffset = { | |
| x: player.x - canvas.width / 2, | |
| y: player.y - canvas.height / 2 | |
| }; | |
| } | |
| resizeCanvas(); | |
| window.addEventListener('resize', resizeCanvas); | |
| // Game state | |
| let gameRunning = false; | |
| let score = 0; | |
| let highScore = localStorage.getItem('highScore') || 0; | |
| highScoreElement.textContent = highScore; | |
| // Player blob | |
| const player = { | |
| x: worldWidth / 2, | |
| y: worldHeight / 2, | |
| radius: 20, | |
| color: '#3b82f6', | |
| speed: 5, | |
| targetX: worldWidth / 2, | |
| targetY: worldHeight / 2 | |
| }; | |
| // Food blobs | |
| let food = []; | |
| const foodColors = ['#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#8b5cf6', '#ec4899']; | |
| // Enemy blobs | |
| let enemies = []; | |
| const enemyColors = ['#dc2626', '#ea580c', '#059669', '#1d4ed8', '#7e22ce', '#be185d']; | |
| // Mouse position | |
| let mouseX = player.x; | |
| let mouseY = player.y; | |
| // Initialize game | |
| function initGame() { | |
| score = 0; | |
| scoreElement.textContent = score; | |
| player.radius = 20; | |
| player.x = canvas.width / 2; | |
| player.y = canvas.height / 2; | |
| food = []; | |
| enemies = []; | |
| // Create initial food | |
| for (let i = 0; i < 50; i++) { | |
| createFood(); | |
| } | |
| // Create initial enemies | |
| for (let i = 0; i < 5; i++) { | |
| createEnemy(); | |
| } | |
| } | |
| // Create food blob | |
| function createFood() { | |
| const radius = Math.random() * 10 + 5; | |
| food.push({ | |
| x: Math.random() * canvas.width, | |
| y: Math.random() * canvas.height, | |
| radius: radius, | |
| color: foodColors[Math.floor(Math.random() * foodColors.length)], | |
| speedX: (Math.random() - 0.5) * 2, | |
| speedY: (Math.random() - 0.5) * 2 | |
| }); | |
| } | |
| // Create enemy blob | |
| function createEnemy() { | |
| const radius = Math.random() * 30 + 20; | |
| enemies.push({ | |
| x: Math.random() * worldWidth, | |
| y: Math.random() * worldHeight, | |
| radius: radius, | |
| color: enemyColors[Math.floor(Math.random() * enemyColors.length)], | |
| speedX: (Math.random() - 0.5) * 2, | |
| speedY: (Math.random() - 0.5) * 2, | |
| state: 'wander', // 'wander' or 'chase' | |
| chaseTimer: 0 | |
| }); | |
| } | |
| // Check collision between two circles | |
| function checkCollision(circle1, circle2) { | |
| const dx = circle1.x - circle2.x; | |
| const dy = circle1.y - circle2.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| return distance < circle1.radius + circle2.radius; | |
| } | |
| // Game loop | |
| function gameLoop() { | |
| if (!gameRunning) return; | |
| // Clear canvas | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Update camera to follow player | |
| cameraOffset.x = player.x - canvas.width / 2; | |
| cameraOffset.y = player.y - canvas.height / 2; | |
| // Apply jiggle effect if active | |
| if (jiggleTime > 0) { | |
| jiggleTime -= 1/60; | |
| const intensity = (jiggleTime / jiggleDuration) * 10; | |
| cameraOffset.x += Math.sin(jiggleTime * 20) * intensity; | |
| cameraOffset.y += Math.cos(jiggleTime * 20) * intensity; | |
| } | |
| // Save context and apply camera transform | |
| ctx.save(); | |
| ctx.translate(-cameraOffset.x, -cameraOffset.y); | |
| // Convert mouse position to world coordinates | |
| const worldMouseX = mouseX + cameraOffset.x; | |
| const worldMouseY = mouseY + cameraOffset.y; | |
| // Move player toward mouse in world space | |
| const dx = worldMouseX - player.x; | |
| const dy = worldMouseY - player.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance > 5) { | |
| player.x += dx / distance * player.speed; | |
| player.y += dy / distance * player.speed; | |
| } | |
| // Keep player within world bounds | |
| player.x = Math.max(player.radius, Math.min(worldWidth - player.radius, player.x)); | |
| player.y = Math.max(player.radius, Math.min(worldHeight - player.radius, player.y)); | |
| // Draw player | |
| ctx.beginPath(); | |
| ctx.arc(player.x, player.y, player.radius, 0, Math.PI * 2); | |
| ctx.fillStyle = player.color; | |
| ctx.fill(); | |
| ctx.closePath(); | |
| // Draw food | |
| for (let i = 0; i < food.length; i++) { | |
| const blob = food[i]; | |
| // Move food | |
| blob.x += blob.speedX; | |
| blob.y += blob.speedY; | |
| // Bounce off walls | |
| if (blob.x - blob.radius < 0 || blob.x + blob.radius > canvas.width) { | |
| blob.speedX = -blob.speedX; | |
| } | |
| if (blob.y - blob.radius < 0 || blob.y + blob.radius > canvas.height) { | |
| blob.speedY = -blob.speedY; | |
| } | |
| // Draw food | |
| ctx.beginPath(); | |
| ctx.arc(blob.x, blob.y, blob.radius, 0, Math.PI * 2); | |
| ctx.fillStyle = blob.color; | |
| ctx.fill(); | |
| ctx.closePath(); | |
| // Check collision with player | |
| if (checkCollision(player, blob)) { | |
| // Player can only eat smaller or equal size food | |
| if (player.radius >= blob.radius) { | |
| // Increase player size and score | |
| player.radius += blob.radius * 0.2; | |
| score += Math.floor(blob.radius); | |
| scoreElement.textContent = score; | |
| // Remove food and create new one | |
| food.splice(i, 1); | |
| createFood(); | |
| i--; | |
| // Trigger jiggle effect | |
| jiggleTime = jiggleDuration; | |
| } | |
| } | |
| } | |
| // Draw enemies | |
| for (let i = 0; i < enemies.length; i++) { | |
| const enemy = enemies[i]; | |
| // AI behavior for enemies | |
| const distToPlayer = Math.sqrt( | |
| Math.pow(enemy.x - player.x, 2) + | |
| Math.pow(enemy.y - player.y, 2) | |
| ); | |
| // If player is close and bigger, chase them | |
| if (distToPlayer < 300 && player.radius > enemy.radius * 1.2) { | |
| enemy.state = 'chase'; | |
| enemy.chaseTimer = 3; // Chase for 3 seconds | |
| // Move toward player | |
| const angle = Math.atan2(player.y - enemy.y, player.x - enemy.x); | |
| enemy.speedX = Math.cos(angle) * 2; | |
| enemy.speedY = Math.sin(angle) * 2; | |
| } | |
| // If chasing but timer expired or player got too far/small | |
| else if (enemy.state === 'chase') { | |
| enemy.chaseTimer -= 1/60; | |
| if (enemy.chaseTimer <= 0 || distToPlayer > 400 || player.radius < enemy.radius) { | |
| enemy.state = 'wander'; | |
| // Random new direction | |
| enemy.speedX = (Math.random() - 0.5) * 2; | |
| enemy.speedY = (Math.random() - 0.5) * 2; | |
| } | |
| } | |
| // Move enemy | |
| enemy.x += enemy.speedX; | |
| enemy.y += enemy.speedY; | |
| // Bounce off world walls | |
| if (enemy.x - enemy.radius < 0 || enemy.x + enemy.radius > worldWidth) { | |
| enemy.speedX = -enemy.speedX; | |
| } | |
| if (enemy.y - enemy.radius < 0 || enemy.y + enemy.radius > worldHeight) { | |
| enemy.speedY = -enemy.speedY; | |
| } | |
| // Draw enemy | |
| ctx.beginPath(); | |
| ctx.arc(enemy.x, enemy.y, enemy.radius, 0, Math.PI * 2); | |
| ctx.fillStyle = enemy.color; | |
| ctx.fill(); | |
| ctx.closePath(); | |
| // Check collision with player | |
| if (checkCollision(player, enemy)) { | |
| // Enemy can eat player if larger | |
| if (enemy.radius > player.radius * 1.1) { | |
| gameOver(); | |
| return; | |
| } | |
| // Player can eat enemy if significantly larger | |
| else if (player.radius > enemy.radius * 1.5) { | |
| // Increase player size and score | |
| player.radius += enemy.radius * 0.3; | |
| score += Math.floor(enemy.radius * 2); | |
| scoreElement.textContent = score; | |
| // Remove enemy and create new one | |
| enemies.splice(i, 1); | |
| createEnemy(); | |
| i--; | |
| // Trigger jiggle effect | |
| jiggleTime = jiggleDuration; | |
| } | |
| } | |
| // Check collision with food | |
| for (let j = 0; j < food.length; j++) { | |
| if (checkCollision(enemy, food[j]) && enemy.radius > food[j].radius) { | |
| // Enemy eats food | |
| enemy.radius += food[j].radius * 0.1; | |
| food.splice(j, 1); | |
| createFood(); | |
| j--; | |
| } | |
| } | |
| } | |
| // Occasionally add more food and enemies | |
| if (Math.random() < 0.01) { | |
| createFood(); | |
| } | |
| if (Math.random() < 0.005) { | |
| createEnemy(); | |
| } | |
| // Restore context (after drawing everything) | |
| ctx.restore(); | |
| requestAnimationFrame(gameLoop); | |
| } | |
| // Game over | |
| function gameOver() { | |
| gameRunning = false; | |
| finalScoreElement.textContent = score; | |
| // Update high score | |
| if (score > highScore) { | |
| highScore = score; | |
| highScoreElement.textContent = highScore; | |
| localStorage.setItem('highScore', highScore); | |
| } | |
| gameOverScreen.classList.remove('hidden'); | |
| } | |
| // Event listeners | |
| canvas.addEventListener('mousemove', (e) => { | |
| mouseX = e.clientX; | |
| mouseY = e.clientY; | |
| }); | |
| // Touch support for mobile | |
| canvas.addEventListener('touchmove', (e) => { | |
| e.preventDefault(); | |
| mouseX = e.touches[0].clientX; | |
| mouseY = e.touches[0].clientY; | |
| }, { passive: false }); | |
| startBtn.addEventListener('click', () => { | |
| startScreen.classList.add('hidden'); | |
| gameRunning = true; | |
| initGame(); | |
| gameLoop(); | |
| }); | |
| restartBtn.addEventListener('click', () => { | |
| gameOverScreen.classList.add('hidden'); | |
| gameRunning = true; | |
| initGame(); | |
| gameLoop(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |