Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>3D Agar.io Game</title> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
background: #000; | |
font-family: 'Arial', sans-serif; | |
overflow: hidden; | |
cursor: none; | |
} | |
#gameContainer { | |
position: relative; | |
width: 100vw; | |
height: 100vh; | |
} | |
#ui { | |
position: absolute; | |
top: 20px; | |
left: 20px; | |
color: white; | |
z-index: 100; | |
text-shadow: 2px 2px 4px rgba(0,0,0,0.8); | |
} | |
#score { | |
font-size: 24px; | |
font-weight: bold; | |
margin-bottom: 10px; | |
} | |
#instructions { | |
font-size: 14px; | |
opacity: 0.8; | |
} | |
#gameOver { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
text-align: center; | |
color: white; | |
background: rgba(0,0,0,0.8); | |
padding: 30px; | |
border-radius: 10px; | |
z-index: 200; | |
display: none; | |
} | |
#gameOver h2 { | |
margin-top: 0; | |
color: #ff4444; | |
font-size: 36px; | |
} | |
#restartBtn { | |
background: #4CAF50; | |
color: white; | |
border: none; | |
padding: 12px 24px; | |
font-size: 16px; | |
border-radius: 5px; | |
cursor: pointer; | |
margin-top: 15px; | |
} | |
#restartBtn:hover { | |
background: #45a049; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="gameContainer"> | |
<div id="ui"> | |
<div id="score">Score: 0</div> | |
<div id="instructions"> | |
Move: Mouse or WASD<br> | |
Fire: SPACE (splits larger enemies!)<br> | |
Eat smaller cells and pellets to grow!<br> | |
Avoid larger cells! | |
</div> | |
</div> | |
<div id="gameOver"> | |
<h2>Game Over!</h2> | |
<p>You were eaten by a larger cell!</p> | |
<div>Final Score: <span id="finalScore">0</span></div> | |
<button id="restartBtn">Play Again</button> | |
</div> | |
</div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
<script> | |
class AgarGame { | |
constructor() { | |
this.scene = new THREE.Scene(); | |
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
this.renderer = new THREE.WebGLRenderer({ antialias: true }); | |
this.clock = new THREE.Clock(); | |
this.worldSize = 200; | |
this.gameRunning = true; | |
this.score = 0; | |
this.player = null; | |
this.aiCells = []; | |
this.foodPellets = []; | |
this.projectiles = []; | |
this.targetPosition = new THREE.Vector3(); | |
this.lastMoveDirection = new THREE.Vector3(0, 0, 1); | |
this.keys = { | |
w: false, | |
a: false, | |
s: false, | |
d: false, | |
space: false | |
}; | |
this.lastSpacePress = false; | |
this.mouse = new THREE.Vector2(); | |
this.raycaster = new THREE.Raycaster(); | |
this.init(); | |
this.setupEventListeners(); | |
this.animate(); | |
} | |
init() { | |
// Renderer setup | |
this.renderer.setSize(window.innerWidth, window.innerHeight); | |
this.renderer.setClearColor(0x001122); | |
this.renderer.shadowMap.enabled = true; | |
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
document.getElementById('gameContainer').appendChild(this.renderer.domElement); | |
// Lighting | |
const ambientLight = new THREE.AmbientLight(0x404040, 0.4); | |
this.scene.add(ambientLight); | |
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
directionalLight.position.set(50, 100, 50); | |
directionalLight.castShadow = true; | |
directionalLight.shadow.mapSize.width = 2048; | |
directionalLight.shadow.mapSize.height = 2048; | |
directionalLight.shadow.camera.near = 0.5; | |
directionalLight.shadow.camera.far = 500; | |
directionalLight.shadow.camera.left = -100; | |
directionalLight.shadow.camera.right = 100; | |
directionalLight.shadow.camera.top = 100; | |
directionalLight.shadow.camera.bottom = -100; | |
this.scene.add(directionalLight); | |
// Ground plane | |
const groundGeometry = new THREE.PlaneGeometry(this.worldSize * 2, this.worldSize * 2); | |
const groundMaterial = new THREE.MeshLambertMaterial({ | |
color: 0x003366, | |
transparent: true, | |
opacity: 0.6 | |
}); | |
const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
ground.rotation.x = -Math.PI / 2; | |
ground.receiveShadow = true; | |
this.scene.add(ground); | |
// Grid helper | |
const gridHelper = new THREE.GridHelper(this.worldSize * 2, 40, 0x004477, 0x002244); | |
this.scene.add(gridHelper); | |
this.createPlayer(); | |
this.createAICells(); | |
this.createFoodPellets(); | |
// Camera setup | |
this.camera.position.set(0, 30, 30); | |
this.updateCamera(); | |
} | |
createPlayer() { | |
const geometry = new THREE.SphereGeometry(2, 16, 16); | |
const material = new THREE.MeshPhongMaterial({ | |
color: 0x00ff44, | |
shininess: 100, | |
specular: 0x111111 | |
}); | |
this.player = new THREE.Mesh(geometry, material); | |
this.player.position.set(0, 2, 0); | |
this.player.castShadow = true; | |
this.player.userData = { | |
size: 2, | |
speed: 0.25, | |
isPlayer: true, | |
mass: 4 | |
}; | |
this.scene.add(this.player); | |
} | |
createAICells() { | |
const colors = [0xff4444, 0x4444ff, 0xffff44, 0xff44ff, 0x44ffff, 0xffa500, 0x800080, 0xffc0cb]; | |
for (let i = 0; i < 15; i++) { | |
const size = Math.random() * 4 + 1; | |
const geometry = new THREE.SphereGeometry(size, 12, 12); | |
const material = new THREE.MeshPhongMaterial({ | |
color: colors[Math.floor(Math.random() * colors.length)], | |
shininess: 80, | |
specular: 0x111111 | |
}); | |
const aiCell = new THREE.Mesh(geometry, material); | |
aiCell.position.set( | |
(Math.random() - 0.5) * this.worldSize, | |
size, | |
(Math.random() - 0.5) * this.worldSize | |
); | |
aiCell.castShadow = true; | |
aiCell.userData = { | |
size: size, | |
speed: Math.max(0.04, 0.18 - size * 0.015), | |
isPlayer: false, | |
direction: new THREE.Vector3( | |
(Math.random() - 0.5) * 2, | |
0, | |
(Math.random() - 0.5) * 2 | |
).normalize(), | |
changeDirectionTimer: 0, | |
mass: size * size | |
}; | |
this.aiCells.push(aiCell); | |
this.scene.add(aiCell); | |
} | |
} | |
createFoodPellets() { | |
for (let i = 0; i < 100; i++) { | |
this.spawnFoodPellet(); | |
} | |
} | |
spawnFoodPellet() { | |
const geometry = new THREE.SphereGeometry(0.3, 8, 8); | |
const material = new THREE.MeshPhongMaterial({ | |
color: new THREE.Color().setHSL(Math.random(), 0.8, 0.6), | |
shininess: 100 | |
}); | |
const pellet = new THREE.Mesh(geometry, material); | |
pellet.position.set( | |
(Math.random() - 0.5) * this.worldSize, | |
0.3, | |
(Math.random() - 0.5) * this.worldSize | |
); | |
pellet.castShadow = true; | |
pellet.userData = { size: 0.3, mass: 0.1 }; | |
this.foodPellets.push(pellet); | |
this.scene.add(pellet); | |
} | |
setupEventListeners() { | |
document.addEventListener('keydown', (event) => { | |
switch(event.code) { | |
case 'KeyW': this.keys.w = true; break; | |
case 'KeyA': this.keys.a = true; break; | |
case 'KeyS': this.keys.s = true; break; | |
case 'KeyD': this.keys.d = true; break; | |
case 'Space': | |
event.preventDefault(); | |
this.keys.space = true; | |
break; | |
} | |
}); | |
document.addEventListener('keyup', (event) => { | |
switch(event.code) { | |
case 'KeyW': this.keys.w = false; break; | |
case 'KeyA': this.keys.a = false; break; | |
case 'KeyS': this.keys.s = false; break; | |
case 'KeyD': this.keys.d = false; break; | |
case 'Space': | |
event.preventDefault(); | |
this.keys.space = false; | |
break; | |
} | |
}); | |
document.addEventListener('mousemove', (event) => { | |
this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1; | |
this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; | |
}); | |
document.getElementById('restartBtn').addEventListener('click', () => { | |
this.restart(); | |
}); | |
window.addEventListener('resize', () => { | |
this.camera.aspect = window.innerWidth / window.innerHeight; | |
this.camera.updateProjectionMatrix(); | |
this.renderer.setSize(window.innerWidth, window.innerHeight); | |
}); | |
} | |
updatePlayer() { | |
if (!this.gameRunning || !this.player) return; | |
const moveDirection = new THREE.Vector3(); | |
// Keyboard movement | |
if (this.keys.w) moveDirection.z -= 1; | |
if (this.keys.s) moveDirection.z += 1; | |
if (this.keys.a) moveDirection.x -= 1; | |
if (this.keys.d) moveDirection.x += 1; | |
// Mouse movement | |
if (moveDirection.length() === 0) { | |
this.raycaster.setFromCamera(this.mouse, this.camera); | |
const intersect = this.raycaster.ray.intersectPlane( | |
new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), | |
new THREE.Vector3() | |
); | |
if (intersect) { | |
this.targetPosition.copy(intersect); | |
moveDirection.copy(this.targetPosition).sub(this.player.position); | |
moveDirection.y = 0; | |
moveDirection.normalize(); | |
} | |
} | |
if (moveDirection.length() > 0) { | |
this.lastMoveDirection.copy(moveDirection); | |
moveDirection.normalize(); | |
const speed = this.player.userData.speed; | |
this.player.position.add(moveDirection.multiplyScalar(speed)); | |
// Keep player in bounds | |
this.player.position.x = Math.max(-this.worldSize, Math.min(this.worldSize, this.player.position.x)); | |
this.player.position.z = Math.max(-this.worldSize, Math.min(this.worldSize, this.player.position.z)); | |
} | |
// Handle firing | |
if (this.keys.space && !this.lastSpacePress) { | |
this.fireProjectile(); | |
} | |
this.lastSpacePress = this.keys.space; | |
} | |
fireProjectile() { | |
if (!this.player || this.player.userData.size < 1.5) return; | |
const geometry = new THREE.SphereGeometry(0.2, 8, 8); | |
const material = new THREE.MeshPhongMaterial({ | |
color: 0xffff00, | |
emissive: 0x444400, | |
shininess: 100 | |
}); | |
const projectile = new THREE.Mesh(geometry, material); | |
projectile.position.copy(this.player.position); | |
projectile.position.y += 1; | |
projectile.castShadow = true; | |
projectile.userData = { | |
direction: this.lastMoveDirection.clone(), | |
speed: 0.8, | |
life: 100, | |
size: 0.2 | |
}; | |
this.projectiles.push(projectile); | |
this.scene.add(projectile); | |
// Reduce player size when firing | |
this.player.userData.size = Math.max(1, this.player.userData.size - 0.1); | |
this.player.userData.mass = this.player.userData.size * this.player.userData.size; | |
this.player.scale.set( | |
this.player.userData.size / 2, | |
this.player.userData.size / 2, | |
this.player.userData.size / 2 | |
); | |
this.player.position.y = this.player.userData.size; | |
} | |
updateProjectiles() { | |
for (let i = this.projectiles.length - 1; i >= 0; i--) { | |
const projectile = this.projectiles[i]; | |
// Move projectile | |
const moveVector = projectile.userData.direction.clone().multiplyScalar(projectile.userData.speed); | |
projectile.position.add(moveVector); | |
projectile.userData.life--; | |
// Remove if expired or out of bounds | |
if (projectile.userData.life <= 0 || | |
Math.abs(projectile.position.x) > this.worldSize || | |
Math.abs(projectile.position.z) > this.worldSize) { | |
this.scene.remove(projectile); | |
this.projectiles.splice(i, 1); | |
continue; | |
} | |
// Check collision with AI cells | |
for (let j = 0; j < this.aiCells.length; j++) { | |
const aiCell = this.aiCells[j]; | |
const distance = projectile.position.distanceTo(aiCell.position); | |
if (distance < projectile.userData.size + aiCell.userData.size) { | |
// Only split if AI cell is larger than player | |
if (aiCell.userData.mass > this.player.userData.mass) { | |
this.splitAICell(aiCell, j); | |
} | |
this.scene.remove(projectile); | |
this.projectiles.splice(i, 1); | |
break; | |
} | |
} | |
} | |
} | |
splitAICell(aiCell, index) { | |
const originalSize = aiCell.userData.size; | |
const newSize = originalSize * 0.7; | |
const originalColor = aiCell.material.color.clone(); | |
// Remove original cell | |
this.scene.remove(aiCell); | |
this.aiCells.splice(index, 1); | |
// Create two new cells | |
for (let i = 0; i < 2; i++) { | |
const geometry = new THREE.SphereGeometry(newSize, 12, 12); | |
const material = new THREE.MeshPhongMaterial({ | |
color: originalColor, | |
shininess: 80, | |
specular: 0x111111 | |
}); | |
const newCell = new THREE.Mesh(geometry, material); | |
// Position the new cells apart | |
const angle = i * Math.PI + Math.random() * 0.5; | |
const distance = (newSize + originalSize) * 1.5; | |
newCell.position.copy(aiCell.position); | |
newCell.position.x += Math.cos(angle) * distance; | |
newCell.position.z += Math.sin(angle) * distance; | |
newCell.position.y = newSize; | |
// Keep in bounds | |
newCell.position.x = Math.max(-this.worldSize, Math.min(this.worldSize, newCell.position.x)); | |
newCell.position.z = Math.max(-this.worldSize, Math.min(this.worldSize, newCell.position.z)); | |
newCell.castShadow = true; | |
newCell.userData = { | |
size: newSize, | |
speed: Math.max(0.04, 0.18 - newSize * 0.015), | |
isPlayer: false, | |
direction: new THREE.Vector3( | |
Math.cos(angle + Math.PI/2), | |
0, | |
Math.sin(angle + Math.PI/2) | |
).normalize(), | |
changeDirectionTimer: 0, | |
mass: newSize * newSize | |
}; | |
this.aiCells.push(newCell); | |
this.scene.add(newCell); | |
} | |
this.score += Math.floor(originalSize * 5); | |
} | |
updateAI() { | |
this.aiCells.forEach(aiCell => { | |
aiCell.userData.changeDirectionTimer += this.clock.getDelta(); | |
if (aiCell.userData.changeDirectionTimer > 3 + Math.random() * 2) { | |
let nearestFood = null; | |
let nearestDistance = Infinity; | |
this.foodPellets.forEach(pellet => { | |
const distance = aiCell.position.distanceTo(pellet.position); | |
if (distance < 15 && distance < nearestDistance) { | |
nearestDistance = distance; | |
nearestFood = pellet; | |
} | |
}); | |
if (nearestFood) { | |
aiCell.userData.direction.copy(nearestFood.position).sub(aiCell.position).normalize(); | |
} else { | |
aiCell.userData.direction.set( | |
(Math.random() - 0.5) * 2, | |
0, | |
(Math.random() - 0.5) * 2 | |
).normalize(); | |
} | |
aiCell.userData.changeDirectionTimer = 0; | |
} | |
const moveVector = aiCell.userData.direction.clone().multiplyScalar(aiCell.userData.speed); | |
aiCell.position.add(moveVector); | |
if (Math.abs(aiCell.position.x) > this.worldSize || Math.abs(aiCell.position.z) > this.worldSize) { | |
aiCell.userData.direction.multiplyScalar(-1); | |
} | |
aiCell.position.x = Math.max(-this.worldSize, Math.min(this.worldSize, aiCell.position.x)); | |
aiCell.position.z = Math.max(-this.worldSize, Math.min(this.worldSize, aiCell.position.z)); | |
}); | |
} | |
checkCollisions() { | |
if (!this.gameRunning || !this.player) return; | |
// Player vs Food | |
for (let i = this.foodPellets.length - 1; i >= 0; i--) { | |
const pellet = this.foodPellets[i]; | |
const distance = this.player.position.distanceTo(pellet.position); | |
if (distance < this.player.userData.size + pellet.userData.size) { | |
this.scene.remove(pellet); | |
this.foodPellets.splice(i, 1); | |
this.growPlayer(0.1); | |
this.score += 1; | |
this.spawnFoodPellet(); | |
} | |
} | |
// Player vs AI cells | |
this.aiCells.forEach((aiCell, index) => { | |
const distance = this.player.position.distanceTo(aiCell.position); | |
const combinedSize = this.player.userData.size + aiCell.userData.size; | |
if (distance < combinedSize * 0.8) { | |
if (this.player.userData.mass > aiCell.userData.mass * 1.2) { | |
this.scene.remove(aiCell); | |
this.aiCells.splice(index, 1); | |
this.growPlayer(aiCell.userData.size * 0.5); | |
this.score += Math.floor(aiCell.userData.size * 10); | |
this.spawnNewAICell(); | |
} else if (aiCell.userData.mass > this.player.userData.mass * 1.2) { | |
this.gameOver(); | |
} | |
} | |
}); | |
// AI vs Food | |
for (let i = this.foodPellets.length - 1; i >= 0; i--) { | |
const pellet = this.foodPellets[i]; | |
for (let j = 0; j < this.aiCells.length; j++) { | |
const aiCell = this.aiCells[j]; | |
const distance = aiCell.position.distanceTo(pellet.position); | |
if (distance < aiCell.userData.size + pellet.userData.size) { | |
this.scene.remove(pellet); | |
this.foodPellets.splice(i, 1); | |
this.growAICell(aiCell, 0.05); | |
this.spawnFoodPellet(); | |
break; | |
} | |
} | |
} | |
// AI vs AI | |
for (let i = 0; i < this.aiCells.length; i++) { | |
for (let j = i + 1; j < this.aiCells.length; j++) { | |
const cell1 = this.aiCells[i]; | |
const cell2 = this.aiCells[j]; | |
const distance = cell1.position.distanceTo(cell2.position); | |
const combinedSize = cell1.userData.size + cell2.userData.size; | |
if (distance < combinedSize * 0.8) { | |
if (cell1.userData.mass > cell2.userData.mass * 1.3) { | |
this.scene.remove(cell2); | |
this.aiCells.splice(j, 1); | |
this.growAICell(cell1, cell2.userData.size * 0.3); | |
this.spawnNewAICell(); | |
break; | |
} else if (cell2.userData.mass > cell1.userData.mass * 1.3) { | |
this.scene.remove(cell1); | |
this.aiCells.splice(i, 1); | |
this.growAICell(cell2, cell1.userData.size * 0.3); | |
this.spawnNewAICell(); | |
break; | |
} | |
} | |
} | |
} | |
} | |
growPlayer(amount) { | |
this.player.userData.size += amount; | |
this.player.userData.mass = this.player.userData.size * this.player.userData.size; | |
this.player.userData.speed = Math.max(0.08, 0.25 - this.player.userData.size * 0.015); | |
this.player.scale.set( | |
this.player.userData.size / 2, | |
this.player.userData.size / 2, | |
this.player.userData.size / 2 | |
); | |
this.player.position.y = this.player.userData.size; | |
} | |
growAICell(aiCell, amount) { | |
aiCell.userData.size += amount; | |
aiCell.userData.mass = aiCell.userData.size * aiCell.userData.size; | |
aiCell.userData.speed = Math.max(0.04, 0.18 - aiCell.userData.size * 0.015); | |
aiCell.scale.set( | |
aiCell.userData.size, | |
aiCell.userData.size, | |
aiCell.userData.size | |
); | |
aiCell.position.y = aiCell.userData.size; | |
} | |
spawnNewAICell() { | |
const colors = [0xff4444, 0x4444ff, 0xffff44, 0xff44ff, 0x44ffff, 0xffa500, 0x800080, 0xffc0cb]; | |
const size = Math.random() * 3 + 1; | |
const geometry = new THREE.SphereGeometry(size, 12, 12); | |
const material = new THREE.MeshPhongMaterial({ | |
color: colors[Math.floor(Math.random() * colors.length)], | |
shininess: 80, | |
specular: 0x111111 | |
}); | |
const aiCell = new THREE.Mesh(geometry, material); | |
const angle = Math.random() * Math.PI * 2; | |
aiCell.position.set( | |
Math.cos(angle) * this.worldSize * 0.9, | |
size, | |
Math.sin(angle) * this.worldSize * 0.9 | |
); | |
aiCell.castShadow = true; | |
aiCell.userData = { | |
size: size, | |
speed: Math.max(0.04, 0.18 - size * 0.015), | |
isPlayer: false, | |
direction: new THREE.Vector3( | |
(Math.random() - 0.5) * 2, | |
0, | |
(Math.random() - 0.5) * 2 | |
).normalize(), | |
changeDirectionTimer: 0, | |
mass: size * size | |
}; | |
this.aiCells.push(aiCell); | |
this.scene.add(aiCell); | |
} | |
updateCamera() { | |
if (!this.player) return; | |
const targetCameraPosition = new THREE.Vector3( | |
this.player.position.x, | |
this.player.position.y + 20 + this.player.userData.size * 3, | |
this.player.position.z + 15 + this.player.userData.size * 2 | |
); | |
this.camera.position.lerp(targetCameraPosition, 0.05); | |
this.camera.lookAt(this.player.position); | |
} | |
updateUI() { | |
document.getElementById('score').textContent = `Score: ${this.score}`; | |
} | |
gameOver() { | |
this.gameRunning = false; | |
document.getElementById('finalScore').textContent = this.score; | |
document.getElementById('gameOver').style.display = 'block'; | |
} | |
restart() { | |
// Clear existing objects | |
if (this.player) { | |
this.scene.remove(this.player); | |
this.player = null; | |
} | |
this.aiCells.forEach(cell => this.scene.remove(cell)); | |
this.aiCells = []; | |
this.foodPellets.forEach(pellet => this.scene.remove(pellet)); | |
this.foodPellets = []; | |
this.projectiles.forEach(projectile => this.scene.remove(projectile)); | |
this.projectiles = []; | |
// Reset game state | |
this.gameRunning = true; | |
this.score = 0; | |
this.lastMoveDirection.set(0, 0, 1); | |
// Recreate game objects | |
this.createPlayer(); | |
this.createAICells(); | |
this.createFoodPellets(); | |
// Hide game over screen | |
document.getElementById('gameOver').style.display = 'none'; | |
} | |
animate() { | |
requestAnimationFrame(() => this.animate()); | |
if (this.gameRunning) { | |
this.updatePlayer(); | |
this.updateAI(); | |
this.updateProjectiles(); | |
this.checkCollisions(); | |
this.updateCamera(); | |
this.updateUI(); | |
} | |
this.renderer.render(this.scene, this.camera); | |
} | |
} | |
// Start the game | |
window.addEventListener('load', () => { | |
new AgarGame(); | |
}); | |
</script> | |
</body> | |
</html> |