awacke1's picture
Update index.html
30b7648 verified
raw
history blame
27.2 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Yar's Revenge 3D</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
body { margin: 0; overflow: hidden; font-family: 'Arial', sans-serif; background-color: #000; color: #fff; }
canvas { display: block; }
#infoPanel {
position: absolute;
top: 10px;
left: 10px;
padding: 10px;
background-color: rgba(0,0,0,0.7);
border-radius: 8px;
color: #fff;
font-size: 16px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 10;
}
.player-info { padding: 5px; border-radius: 4px; }
.player1 { background-color: rgba(0, 150, 255, 0.5); }
.player2 { background-color: rgba(255, 100, 0, 0.5); }
.health-bar-container {
position: absolute;
width: 100px;
height: 16px;
background-color: #333;
border: 1px solid #777;
border-radius: 4px;
overflow: hidden;
text-align: center;
color: white;
font-size: 12px;
line-height: 14px;
transform: translateX(-50%); /* Center horizontally */
z-index: 20;
display: none; /* Hidden by default, controlled by JS */
}
.health-bar-fill {
height: 100%;
background-color: #4caf50; /* Green */
width: 100%; /* Full by default */
transition: width 0.2s ease-in-out;
}
#gameOverScreen {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
padding: 30px;
background-color: rgba(20, 20, 20, 0.9);
border: 2px solid #555;
border-radius: 15px;
text-align: center;
display: none;
z-index: 100;
}
#gameOverScreen h2 { margin-top: 0; font-size: 28px; color: #ff4444; }
#gameOverScreen p { font-size: 18px; }
#gameOverScreen button {
padding: 12px 25px; font-size: 18px; color: #fff; background-color: #007bff;
border: none; border-radius: 8px; cursor: pointer; margin-top: 20px;
transition: background-color 0.3s ease;
}
#gameOverScreen button:hover { background-color: #0056b3; }
</style>
</head>
<body>
<div id="infoPanel">
<div id="player1Info" class="player-info player1">Player 1 (WASD, E): Score 0</div>
<div id="player2Info" class="player-info player2">Player 2 (IJKL, U): Score 0</div>
</div>
<div id="gameOverScreen">
<h2>Game Over!</h2>
<p id="gameOverMessage"></p>
<button id="restartButton">Restart Game</button>
</div>
<script>
let scene, camera, renderer, clock;
let players = [];
let playerProjectiles = [];
let babyYars = []; // ***** REPLACED enemyProjectiles
let neutralZoneBlocks = [];
let qotile;
const keysPressed = {};
const gameSettings = {
playerSpeed: 10,
projectileSpeed: 30,
babyYarSpeed: 8,
projectileSize: 0.2,
neutralZoneBlockSize: 2,
qotileSize: 4,
playAreaWidth: 40, // Increased play area
playAreaHeight: 30,
playerShootCooldown: 0.2,
qotileSpawnCooldown: 2.5, // How often Qotile spawns a baby
playerInitialHealth: 150, // ***** NEW
qotileInitialHealth: 250, // Increased health
babyYarInitialHealth: 5, // ***** NEW
babyYarDamage: 15, // Damage a baby yar does on collision
pointsPerNeutralBlock: 10,
pointsPerBabyYar: 25,
pointsPerQotileHit: 10, // Reduced points for hitting Qotile directly
};
let gameActive = true;
// ******** NEW: HEALTH BAR MANAGEMENT ********
function createHealthBar(character) {
const container = document.createElement('div');
container.className = 'health-bar-container';
const fill = document.createElement('div');
fill.className = 'health-bar-fill';
container.appendChild(fill);
document.body.appendChild(container);
character.healthBar = {
container: container,
fill: fill,
text: container
};
// Set specific colors for different characters
if (character.isPlayer) {
fill.style.backgroundColor = '#2196F3'; // Blue for players
} else if (character.isBabyYar) {
fill.style.backgroundColor = '#f44336'; // Red for babies
} else { // Qotile
fill.style.backgroundColor = '#ff9800'; // Orange for boss
container.style.width = '200px';
container.style.height = '20px';
container.style.fontSize = '14px';
container.style.lineHeight = '18px';
}
}
function updateHealthBar(character) {
if (!character.healthBar) return;
const healthPercent = (character.health / character.maxHealth) * 100;
character.healthBar.fill.style.width = `${healthPercent}%`;
character.healthBar.text.textContent = `${Math.ceil(character.health)} / ${character.maxHealth}`;
// Position the health bar above the character
const screenPosition = toScreenPosition(character.mesh, camera);
if (screenPosition.z > 1) { // z > 1 means it's clipped, so hide it
character.healthBar.container.style.display = 'none';
} else {
character.healthBar.container.style.display = 'block';
character.healthBar.container.style.left = `${screenPosition.x}px`;
character.healthBar.container.style.top = `${screenPosition.y - 40}px`; // Offset above the model
}
}
function toScreenPosition(obj, camera) {
const vector = new THREE.Vector3();
obj.updateMatrixWorld();
vector.setFromMatrixPosition(obj.matrixWorld);
vector.project(camera);
vector.x = (vector.x * window.innerWidth / 2) + window.innerWidth / 2;
vector.y = -(vector.y * window.innerHeight / 2) + window.innerHeight / 2;
return vector;
}
function init() {
gameActive = true;
scene = new THREE.Scene();
scene.background = new THREE.Color(0x111122);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 8, 30);
camera.lookAt(0, 0, 0);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const ambientLight = new THREE.AmbientLight(0x606060);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(5, 10, 7.5);
scene.add(directionalLight);
clock = new THREE.Clock();
createPlayers();
createNeutralZone();
createQotile();
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);
window.addEventListener('resize', onWindowResize);
document.getElementById('restartButton').addEventListener('click', restartGame);
document.getElementById('gameOverScreen').style.display = 'none';
updateUI();
animate();
}
function createYarModel(color) { /* ... unchanged ... */
const yarGroup = new THREE.Group();
const bodyGeometry = new THREE.CylinderGeometry(0.2, 0.4, 1.5, 8);
const bodyMaterial = new THREE.MeshStandardMaterial({ color: color, roughness: 0.5 });
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
body.rotation.x = Math.PI / 2;
yarGroup.add(body);
const headGeometry = new THREE.ConeGeometry(0.3, 0.6, 8);
const headMaterial = new THREE.MeshStandardMaterial({ color: 0xcccccc, metalness: 0.5 });
const head = new THREE.Mesh(headGeometry, headMaterial);
head.position.z = -1;
head.rotation.x = Math.PI / 2;
yarGroup.add(head);
const wingGeometry = new THREE.PlaneGeometry(1.5, 1);
const wingMaterial = new THREE.MeshStandardMaterial({ color: color, transparent: true, opacity: 0.7, side: THREE.DoubleSide });
const wings = [];
for (let i = 0; i < 4; i++) {
const wing = new THREE.Mesh(wingGeometry, wingMaterial);
wing.position.y = 0.2;
yarGroup.add(wing);
wings.push(wing);
}
wings[0].position.set(-0.8, 0, 0); wings[0].rotation.z = Math.PI / 4;
wings[1].position.set(0.8, 0, 0); wings[1].rotation.z = -Math.PI / 4;
wings[2].position.set(-0.7, 0, 0.5); wings[2].scale.set(0.8, 0.8, 0.8); wings[2].rotation.z = Math.PI / 4;
wings[3].position.set(0.7, 0, 0.5); wings[3].scale.set(0.8, 0.8, 0.8); wings[3].rotation.z = -Math.PI / 4;
yarGroup.userData.wings = wings;
yarGroup.userData.isMoving = false;
return yarGroup;
}
function createPlayers() {
players.forEach(p => { if (p.healthBar) document.body.removeChild(p.healthBar.container); });
players = [];
playerProjectiles = [];
const p1Model = createYarModel(0x0099ff);
p1Model.position.set(-gameSettings.playAreaWidth / 4, 0, 18);
scene.add(p1Model);
const p1 = {
mesh: p1Model, isPlayer: true, isPlayer2: false,
controls: { up: 'KeyW', down: 'KeyS', left: 'KeyA', right: 'KeyD', shoot: 'KeyE' },
shootCooldownTimer: 0, score: 0,
health: gameSettings.playerInitialHealth, maxHealth: gameSettings.playerInitialHealth
};
createHealthBar(p1);
players.push(p1);
const p2Model = createYarModel(0xff6600);
p2Model.position.set(gameSettings.playAreaWidth / 4, 0, 18);
scene.add(p2Model);
const p2 = {
mesh: p2Model, isPlayer: true, isPlayer2: true,
controls: { up: 'KeyI', down: 'KeyK', left: 'KeyJ', right: 'KeyL', shoot: 'KeyU' },
shootCooldownTimer: 0, score: 0,
health: gameSettings.playerInitialHealth, maxHealth: gameSettings.playerInitialHealth
};
createHealthBar(p2);
players.push(p2);
}
function createQotileModel() { /* ... unchanged ... */
const qotileGroup = new THREE.Group();
const coreGeometry = new THREE.DodecahedronGeometry(gameSettings.qotileSize, 0);
const coreMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000, emissive: 0x550000, roughness: 0.2 });
const core = new THREE.Mesh(coreGeometry, coreMaterial);
qotileGroup.add(core);
const eyeGeometry = new THREE.SphereGeometry(0.8, 16, 16);
const eyeMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
const pupilGeometry = new THREE.SphereGeometry(0.3, 12, 12);
const pupilMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
const leftEye = new THREE.Mesh(eyeGeometry, eyeMaterial);
leftEye.position.set(-1.2, 1, -3.5);
const rightEye = new THREE.Mesh(eyeGeometry, eyeMaterial);
rightEye.position.set(1.2, 1, -3.5);
const leftPupil = new THREE.Mesh(pupilGeometry, pupilMaterial);
leftPupil.position.z = -0.6;
const rightPupil = new THREE.Mesh(pupilGeometry, pupilMaterial);
rightPupil.position.z = -0.6;
leftEye.add(leftPupil);
rightEye.add(rightPupil);
qotileGroup.add(leftEye);
qotileGroup.add(rightEye);
qotileGroup.userData.eyes = [leftEye, rightEye];
return qotileGroup;
}
function createNeutralZone() { /* ... unchanged ... */
neutralZoneBlocks.forEach(block => {
if(scene.getObjectById(block.id)) scene.remove(block);
if(block.geometry) block.geometry.dispose(); if(block.material) block.material.dispose();
});
neutralZoneBlocks = [];
const blockGeometry = new THREE.BoxGeometry(gameSettings.neutralZoneBlockSize, gameSettings.neutralZoneBlockSize, gameSettings.neutralZoneBlockSize / 2);
const blockMaterial = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.7, metalness: 0.3 });
const numX = Math.floor(gameSettings.playAreaWidth / gameSettings.neutralZoneBlockSize);
const numY = Math.floor(gameSettings.playAreaHeight / gameSettings.neutralZoneBlockSize);
for (let i = 0; i < numX; i++) {
for (let j = 0; j < numY; j++) {
const block = new THREE.Mesh(blockGeometry.clone(), blockMaterial.clone());
block.position.set( (i - numX / 2 + 0.5) * gameSettings.neutralZoneBlockSize, (j - numY / 2 + 0.5) * gameSettings.neutralZoneBlockSize, 0);
scene.add(block); neutralZoneBlocks.push(block);
}
}
}
function createQotile() {
if (qotile) {
if(qotile.mesh) scene.remove(qotile.mesh);
if(qotile.healthBar) document.body.removeChild(qotile.healthBar.container);
}
const qotileModel = createQotileModel();
qotileModel.position.set(0, 0, -20);
scene.add(qotileModel);
qotile = {
mesh: qotileModel,
health: gameSettings.qotileInitialHealth, maxHealth: gameSettings.qotileInitialHealth,
hitTimer: 0, spawnCooldownTimer: gameSettings.qotileSpawnCooldown,
};
createHealthBar(qotile);
}
// ******** NEW: CREATE BABY YAR MINION ********
function createBabyYar(startPosition) {
const group = createQotileModel(); // Reuse the model but smaller
group.scale.set(0.2, 0.2, 0.2);
group.position.copy(startPosition);
const baby = {
mesh: group, isBabyYar: true,
health: gameSettings.babyYarInitialHealth, maxHealth: gameSettings.babyYarInitialHealth,
velocity: new THREE.Vector3(0, 0, gameSettings.babyYarSpeed)
};
createHealthBar(baby);
scene.add(group);
babyYars.push(baby);
}
function handlePlayerMovement(player, delta) { /* ... unchanged ... */
let moved = false;
const moveDistance = gameSettings.playerSpeed * delta;
if (keysPressed[player.controls.up]) { player.mesh.position.y += moveDistance; moved = true; }
if (keysPressed[player.controls.down]) { player.mesh.position.y -= moveDistance; moved = true; }
if (keysPressed[player.controls.left]) { player.mesh.position.x -= moveDistance; moved = true; }
if (keysPressed[player.controls.right]) { player.mesh.position.x += moveDistance; moved = true; }
player.mesh.userData.isMoving = moved;
const halfWidth = gameSettings.playAreaWidth / 2;
const halfHeight = gameSettings.playAreaHeight / 2;
player.mesh.position.x = Math.max(-halfWidth, Math.min(halfWidth, player.mesh.position.x));
player.mesh.position.y = Math.max(-halfHeight, Math.min(halfHeight, player.mesh.position.y));
}
function handlePlayerShooting(player, delta) { /* ... unchanged ... */
if (player.shootCooldownTimer > 0) player.shootCooldownTimer -= delta;
if (keysPressed[player.controls.shoot] && player.shootCooldownTimer <= 0) {
player.shootCooldownTimer = gameSettings.playerShootCooldown;
createProjectile(player);
}
}
function createProjectile(player) { /* ... unchanged ... */
const projectileGeometry = new THREE.SphereGeometry(gameSettings.projectileSize, 8, 8);
const projectileMaterial = new THREE.MeshBasicMaterial({ color: player.isPlayer2 ? 0xffaa33 : 0x66ccff });
const projectile = new THREE.Mesh(projectileGeometry, projectileMaterial);
const startPosition = player.mesh.position.clone();
startPosition.z -= 1.5;
projectile.position.copy(startPosition);
projectile.userData = { owner: player, velocity: new THREE.Vector3(0, 0, -gameSettings.projectileSpeed) };
scene.add(projectile);
playerProjectiles.push(projectile);
}
function updateProjectilesAndMinions(delta) {
// Player projectiles
for (let i = playerProjectiles.length - 1; i >= 0; i--) {
const p = playerProjectiles[i];
p.position.addScaledVector(p.userData.velocity, delta);
if (p.position.z < -30) {
scene.remove(p); p.geometry.dispose(); p.material.dispose();
playerProjectiles.splice(i, 1); continue;
}
checkPlayerProjectileCollision(p, i);
}
// Baby Yars (Minions)
for (let i = babyYars.length - 1; i >= 0; i--) {
const baby = babyYars[i];
baby.mesh.position.addScaledVector(baby.velocity, delta);
// Animate baby eyes
const time = clock.getElapsedTime();
baby.mesh.userData.eyes.forEach((eye, j) => {
eye.children[0].position.x = Math.sin(time * 5 + j) * 0.15;
eye.children[0].position.y = Math.cos(time * 5 + j) * 0.15;
});
if (baby.mesh.position.z > 30) {
scene.remove(baby.mesh);
document.body.removeChild(baby.healthBar.container);
babyYars.splice(i, 1);
continue;
}
checkBabyYarCollision(baby, i);
}
}
function checkPlayerProjectileCollision(projectile, projectileIndex) {
const pSphere = new THREE.Sphere(projectile.position, gameSettings.projectileSize);
const owner = projectile.userData.owner;
// Vs Neutral Zone
// ... (unchanged logic) ...
// Vs Baby Yars
for (let i = babyYars.length - 1; i >= 0; i--) {
const baby = babyYars[i];
const babyBox = new THREE.Box3().setFromObject(baby.mesh);
if (pSphere.intersectsBox(babyBox)) {
// d4 damage
const damage = Math.floor(Math.random() * 4) + 1;
baby.health -= damage;
if (baby.health <= 0) {
owner.score += gameSettings.pointsPerBabyYar;
scene.remove(baby.mesh);
document.body.removeChild(baby.healthBar.container);
babyYars.splice(i, 1);
}
scene.remove(projectile); projectile.geometry.dispose(); projectile.material.dispose();
playerProjectiles.splice(projectileIndex, 1);
updateUI(); return;
}
}
// Vs Qotile
if (qotile && qotile.health > 0) {
const qotileBox = new THREE.Box3().setFromObject(qotile.mesh);
if (pSphere.intersectsBox(qotileBox)) {
const damage = Math.floor(Math.random() * 4) + 1;
qotile.health -= damage;
owner.score += gameSettings.pointsPerQotileHit;
qotile.mesh.children[0].material.emissive.setHex(0xffffff);
qotile.hitTimer = 0.1;
if (qotile.health <= 0) {
endGame((owner.isPlayer2 ? "Player 2" : "Player 1") + " destroyed the Qotile!");
}
scene.remove(projectile); projectile.geometry.dispose(); projectile.material.dispose();
playerProjectiles.splice(projectileIndex, 1);
updateUI(); return;
}
}
}
// ******** NEW: COLLISION FOR BABY YARS ********
function checkBabyYarCollision(baby, babyIndex) {
const babySphere = new THREE.Sphere(baby.mesh.position, 1.0); // Hitbox size
for(let i = players.length - 1; i >= 0; i--) {
const player = players[i];
if (player.health <= 0) continue;
const playerBox = new THREE.Box3().setFromObject(player.mesh);
if (babySphere.intersectsBox(playerBox)) {
player.health -= gameSettings.babyYarDamage;
// Cleanup baby
scene.remove(baby.mesh);
document.body.removeChild(baby.healthBar.container);
babyYars.splice(babyIndex, 1);
// Player hit effect
player.mesh.visible = false;
setTimeout(() => { player.mesh.visible = true; }, 100);
setTimeout(() => { player.mesh.visible = false; }, 200);
setTimeout(() => { player.mesh.visible = true; }, 300);
if (player.health <= 0) {
player.health = 0;
scene.remove(player.mesh);
document.body.removeChild(player.healthBar.container);
if(players.every(p => p.health <= 0)) {
endGame("The Qotile has defeated all Yars!");
}
}
updateUI(); return;
}
}
}
function updateQotile(delta) {
if (qotile && qotile.mesh) {
const time = clock.getElapsedTime();
qotile.mesh.rotation.y += delta * 0.1;
qotile.mesh.userData.eyes.forEach((eye, i) => {
eye.children[0].position.x = Math.sin(time * 3 + i) * 0.15;
eye.children[0].position.y = Math.cos(time * 3 + i) * 0.15;
});
if (qotile.hitTimer > 0) {
qotile.hitTimer -= delta;
if (qotile.hitTimer <= 0) qotile.mesh.children[0].material.emissive.setHex(0x550000);
}
if (qotile.health > 0) {
qotile.spawnCooldownTimer -= delta;
if (qotile.spawnCooldownTimer <= 0) {
createBabyYar(qotile.mesh.position);
qotile.spawnCooldownTimer = gameSettings.qotileSpawnCooldown;
}
}
}
}
function updatePlayers(delta) { /* ... unchanged wing animation ... */
const time = clock.getElapsedTime();
players.forEach(player => {
if (player.health <= 0) return;
const wings = player.mesh.userData.wings;
const flapSpeed = player.mesh.userData.isMoving ? 15 : 4;
wings[0].rotation.y = Math.sin(time * flapSpeed) * 0.8;
wings[1].rotation.y = -Math.sin(time * flapSpeed) * 0.8;
wings[2].rotation.y = Math.sin(time * flapSpeed + 0.5) * 0.6;
wings[3].rotation.y = -Math.sin(time * flapSpeed + 0.5) * 0.6;
});
}
function updateUI() {
const p1 = players[0] || { score: 0 };
const p2 = players[1] || { score: 0 };
document.getElementById('player1Info').textContent = `Player 1 (WASD, E): Score ${p1.score}`;
document.getElementById('player2Info').textContent = `Player 2 (IJKL, U): Score ${p2.score}`;
// Health bars are updated in the main loop
}
function endGame(message) {
if (!gameActive) return;
gameActive = false;
document.getElementById('gameOverMessage').textContent = message;
document.getElementById('gameOverScreen').style.display = 'flex';
}
function restartGame() {
// Remove all dynamic elements
[...players, ...babyYars].forEach(char => {
if (char.mesh) scene.remove(char.mesh);
if (char.healthBar) document.body.removeChild(char.healthBar.container);
});
if (qotile) {
if(qotile.mesh) scene.remove(qotile.mesh);
if(qotile.healthBar) document.body.removeChild(qotile.healthBar.container);
}
playerProjectiles.forEach(p => { if (p) scene.remove(p); });
players = []; playerProjectiles = []; babyYars = [];
createPlayers();
createNeutralZone();
createQotile();
gameActive = true;
document.getElementById('gameOverScreen').style.display = 'none';
updateUI();
}
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
if (gameActive) {
players.forEach(player => {
if (player.health > 0) {
handlePlayerMovement(player, delta);
handlePlayerShooting(player, delta);
}
updateHealthBar(player);
});
babyYars.forEach(updateHealthBar);
if (qotile) updateHealthBar(qotile);
updateProjectilesAndMinions(delta);
updateQotile(delta);
updatePlayers(delta);
}
renderer.render(scene, camera);
}
function onKeyDown(event) { keysPressed[event.code] = true; }
function onKeyUp(event) { keysPressed[event.code] = false; }
function onWindowResize() { /* ... unchanged ... */ }
window.onload = function() { init(); };
</script>
</body>
</html>