SphereEtersIO / index.html
awacke1's picture
Update index.html
9e3204b verified
<!DOCTYPE html>
<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>