Spaces:
Running
Running
<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; | |
} | |
.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); } | |
#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; /* Hidden by default */ | |
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 | Lives 3</div> | |
<div id="player2Info" class="player-info player2">Player 2 (IJKL, U): Score 0 | Lives 3</div> | |
<div id="qotileInfo">Qotile Health: 100</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 neutralZoneBlocks = []; | |
let qotile; | |
const keysPressed = {}; // Stores the state of currently pressed keys using event.code | |
const gameSettings = { | |
playerSpeed: 10, | |
projectileSpeed: 30, | |
playerSize: 1, | |
projectileSize: 0.2, | |
neutralZoneBlockSize: 2, | |
qotileSize: 4, | |
playAreaWidth: 30, | |
playAreaHeight: 20, | |
playerShootCooldown: 0.2, // seconds | |
initialPlayerLives: 3, | |
qotileInitialHealth: 100, | |
pointsPerNeutralBlock: 10, | |
pointsPerQotileHit: 50, | |
}; | |
let gameActive = true; | |
// Initialization function | |
function init() { | |
gameActive = true; | |
// Scene | |
scene = new THREE.Scene(); | |
scene.background = new THREE.Color(0x111122); | |
// Camera | |
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
camera.position.set(0, 5, 25); // Positioned to see the play area | |
camera.lookAt(0, 0, 0); | |
// Renderer | |
renderer = new THREE.WebGLRenderer({ antialias: true }); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
document.body.appendChild(renderer.domElement); | |
// Lighting | |
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 for delta time | |
clock = new THREE.Clock(); | |
// Create game elements | |
createPlayers(); | |
createNeutralZone(); | |
createQotile(); | |
// Event Listeners | |
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(); | |
} | |
// Create Player objects | |
function createPlayers() { | |
players = []; // Clear existing players if any (for restart) | |
playerProjectiles = []; // Clear existing projectiles | |
const playerGeometry = new THREE.BoxGeometry(gameSettings.playerSize, gameSettings.playerSize, gameSettings.playerSize); | |
// Player 1 | |
const player1Material = new THREE.MeshStandardMaterial({ color: 0x0099ff }); | |
const player1Mesh = new THREE.Mesh(playerGeometry, player1Material); | |
player1Mesh.position.set(-gameSettings.playAreaWidth / 4, 0, 15); | |
scene.add(player1Mesh); | |
players.push({ | |
mesh: player1Mesh, | |
isPlayer2: false, | |
// ******** UPDATED FIRE KEY FOR PLAYER 1 ******** | |
controls: { up: 'KeyW', down: 'KeyS', left: 'KeyA', right: 'KeyD', shoot: 'KeyE' }, | |
shootCooldownTimer: 0, | |
score: 0, | |
lives: gameSettings.initialPlayerLives, | |
projectiles: [] | |
}); | |
// Player 2 | |
const player2Material = new THREE.MeshStandardMaterial({ color: 0xff6600 }); | |
const player2Mesh = new THREE.Mesh(playerGeometry, player2Material); | |
player2Mesh.position.set(gameSettings.playAreaWidth / 4, 0, 15); | |
scene.add(player2Mesh); | |
players.push({ | |
mesh: player2Mesh, | |
isPlayer2: true, | |
// ******** UPDATED FIRE KEY FOR PLAYER 2 ******** | |
controls: { up: 'KeyI', down: 'KeyK', left: 'KeyJ', right: 'KeyL', shoot: 'KeyU' }, | |
shootCooldownTimer: 0, | |
score: 0, | |
lives: gameSettings.initialPlayerLives, | |
projectiles: [] | |
}); | |
} | |
// Create Neutral Zone blocks | |
function createNeutralZone() { | |
// Remove existing blocks from scene before clearing array | |
neutralZoneBlocks.forEach(block => { | |
if (block && scene.getObjectById(block.id)) scene.remove(block); | |
if (block && block.geometry) block.geometry.dispose(); | |
if (block && block.material) block.material.dispose(); | |
}); | |
neutralZoneBlocks = []; // Clear for restart | |
const blockGeometry = new THREE.BoxGeometry( | |
gameSettings.neutralZoneBlockSize, | |
gameSettings.neutralZoneBlockSize, | |
gameSettings.neutralZoneBlockSize / 2 // Thinner blocks | |
); | |
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 // Z-position of the neutral zone | |
); | |
block.userData = { health: 1 }; // Simple health for blocks | |
scene.add(block); | |
neutralZoneBlocks.push(block); | |
} | |
} | |
} | |
// Create Qotile (enemy base) | |
function createQotile() { | |
if (qotile && qotile.mesh) { | |
if (scene.getObjectById(qotile.mesh.id)) scene.remove(qotile.mesh); | |
if (qotile.mesh.geometry) qotile.mesh.geometry.dispose(); | |
if (qotile.mesh.material) qotile.mesh.material.dispose(); | |
} | |
const qotileGeometry = new THREE.BoxGeometry(gameSettings.qotileSize, gameSettings.qotileSize, gameSettings.qotileSize); | |
const qotileMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000, emissive: 0x330000 }); | |
const qotileMesh = new THREE.Mesh(qotileGeometry, qotileMaterial); | |
qotileMesh.position.set(0, 0, -15); // Positioned behind the neutral zone | |
scene.add(qotileMesh); | |
qotile = { | |
mesh: qotileMesh, | |
health: gameSettings.qotileInitialHealth, | |
hitTimer: 0 // For visual feedback on hit | |
}; | |
} | |
// Handle player movement | |
function handlePlayerMovement(player, delta) { | |
const moveDistance = gameSettings.playerSpeed * delta; | |
// Check against the correct event.code strings stored in player.controls | |
if (keysPressed[player.controls.up]) player.mesh.position.y += moveDistance; | |
if (keysPressed[player.controls.down]) player.mesh.position.y -= moveDistance; | |
if (keysPressed[player.controls.left]) player.mesh.position.x -= moveDistance; | |
if (keysPressed[player.controls.right]) player.mesh.position.x += moveDistance; | |
// Boundary checks | |
const halfWidth = gameSettings.playAreaWidth / 2 - gameSettings.playerSize / 2; | |
const halfHeight = gameSettings.playAreaHeight / 2 - gameSettings.playerSize / 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)); | |
} | |
// Handle player shooting | |
function handlePlayerShooting(player, delta) { | |
if (player.shootCooldownTimer > 0) { | |
player.shootCooldownTimer -= delta; | |
} | |
// Check against the correct event.code string for shooting | |
if (keysPressed[player.controls.shoot] && player.shootCooldownTimer <= 0) { | |
player.shootCooldownTimer = gameSettings.playerShootCooldown; | |
createProjectile(player); | |
} | |
} | |
// Create a projectile | |
function createProjectile(player) { | |
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); | |
projectile.position.copy(player.mesh.position); | |
projectile.position.z -= gameSettings.playerSize / 2; // Start in front of player | |
projectile.userData = { | |
owner: player, | |
velocity: new THREE.Vector3(0, 0, -gameSettings.projectileSpeed) // Shoots "into" the screen | |
}; | |
scene.add(projectile); | |
playerProjectiles.push(projectile); | |
} | |
// Update projectiles | |
function updateProjectiles(delta) { | |
for (let i = playerProjectiles.length - 1; i >= 0; i--) { | |
const projectile = playerProjectiles[i]; | |
if (!projectile || !projectile.userData) { // Safety check | |
if (projectile && scene.getObjectById(projectile.id)) scene.remove(projectile); // Clean up from scene if partially added | |
playerProjectiles.splice(i, 1); | |
continue; | |
} | |
projectile.position.addScaledVector(projectile.userData.velocity, delta); | |
// Remove if out of bounds | |
if (projectile.position.z < -30 || projectile.position.z > 30 || | |
Math.abs(projectile.position.x) > gameSettings.playAreaWidth / 2 + 5 || // Added some buffer | |
Math.abs(projectile.position.y) > gameSettings.playAreaHeight / 2 + 5) { | |
scene.remove(projectile); | |
if (projectile.geometry) projectile.geometry.dispose(); | |
if (projectile.material) projectile.material.dispose(); | |
playerProjectiles.splice(i, 1); | |
continue; | |
} | |
checkProjectileCollisions(projectile, i); | |
} | |
} | |
// Check projectile collisions | |
function checkProjectileCollisions(projectile, projectileIndex) { | |
if (!projectile || !projectile.userData || !projectile.userData.owner) { | |
if (projectile && scene.getObjectById(projectile.id)) scene.remove(projectile); | |
if(playerProjectiles[projectileIndex] === projectile) playerProjectiles.splice(projectileIndex, 1); | |
return; | |
} | |
const projectileBox = new THREE.Box3().setFromObject(projectile); | |
// Collision with Neutral Zone blocks | |
for (let j = neutralZoneBlocks.length - 1; j >= 0; j--) { | |
const block = neutralZoneBlocks[j]; | |
const blockBox = new THREE.Box3().setFromObject(block); | |
if (projectileBox.intersectsBox(blockBox)) { | |
scene.remove(block); | |
if (block.geometry) block.geometry.dispose(); | |
if (block.material) block.material.dispose(); | |
neutralZoneBlocks.splice(j, 1); | |
scene.remove(projectile); | |
if (projectile.geometry) projectile.geometry.dispose(); | |
if (projectile.material) projectile.material.dispose(); | |
playerProjectiles.splice(projectileIndex, 1); | |
projectile.userData.owner.score += gameSettings.pointsPerNeutralBlock; | |
updateUI(); | |
return; | |
} | |
} | |
// Collision with Qotile | |
if (qotile && qotile.mesh && qotile.health > 0) { | |
const qotileBox = new THREE.Box3().setFromObject(qotile.mesh); | |
if (projectileBox.intersectsBox(qotileBox)) { | |
scene.remove(projectile); | |
if (projectile.geometry) projectile.geometry.dispose(); | |
if (projectile.material) projectile.material.dispose(); | |
playerProjectiles.splice(projectileIndex, 1); | |
qotile.health -= 10; | |
qotile.mesh.material.emissive.setHex(0xffffff); | |
qotile.hitTimer = 0.1; | |
projectile.userData.owner.score += gameSettings.pointsPerQotileHit; | |
updateUI(); | |
if (qotile.health <= 0) { | |
const winnerName = projectile.userData.owner.isPlayer2 ? "Player 2" : "Player 1"; | |
endGame(winnerName + " destroyed the Qotile!"); | |
} | |
return; | |
} | |
} | |
} | |
function updateQotile(delta) { | |
if (qotile && qotile.hitTimer > 0) { | |
qotile.hitTimer -= delta; | |
if (qotile.hitTimer <= 0) { | |
qotile.mesh.material.emissive.setHex(0x330000); | |
} | |
} | |
} | |
// Update UI elements | |
function updateUI() { | |
const p1 = players && players[0] ? players[0] : { score: 0, lives: 0 }; | |
const p2 = players && players[1] ? players[1] : { score: 0, lives: 0 }; | |
// ******** UPDATED UI TEXT FOR NEW FIRE KEYS ******** | |
document.getElementById('player1Info').textContent = `Player 1 (WASD, E): Score ${p1.score} | Lives ${p1.lives}`; | |
document.getElementById('player2Info').textContent = `Player 2 (IJKL, U): Score ${p2.score} | Lives ${p2.lives}`; | |
if (qotile) { | |
document.getElementById('qotileInfo').textContent = `Qotile Health: ${Math.max(0, qotile.health)}`; | |
} else { | |
document.getElementById('qotileInfo').textContent = `Qotile Health: N/A`; | |
} | |
} | |
function checkGameOver() { | |
if (!gameActive) return; | |
} | |
function endGame(message) { | |
if (!gameActive) return; | |
gameActive = false; | |
console.log("Game Over:", message); | |
document.getElementById('gameOverMessage').textContent = message; | |
document.getElementById('gameOverScreen').style.display = 'flex'; | |
playerProjectiles.forEach(p => { | |
if (p && scene.getObjectById(p.id)) scene.remove(p); | |
if (p && p.geometry) p.geometry.dispose(); | |
if (p && p.material) p.material.dispose(); | |
}); | |
playerProjectiles = []; | |
} | |
function restartGame() { | |
players.forEach(p => { | |
if (p.mesh && scene.getObjectById(p.mesh.id)) scene.remove(p.mesh); | |
if (p.mesh && p.mesh.geometry) p.mesh.geometry.dispose(); | |
if (p.mesh && p.mesh.material) p.mesh.material.dispose(); | |
}); | |
playerProjectiles.forEach(p => { | |
if (p && scene.getObjectById(p.id)) scene.remove(p); | |
if (p && p.geometry) p.geometry.dispose(); | |
if (p && p.material) p.material.dispose(); | |
}); | |
neutralZoneBlocks.forEach(b => { | |
if (b && scene.getObjectById(b.id)) scene.remove(b); | |
if (b && b.geometry) b.geometry.dispose(); | |
if (b && b.material) b.material.dispose(); | |
}); | |
if (qotile && qotile.mesh) { | |
if (scene.getObjectById(qotile.mesh.id)) scene.remove(qotile.mesh); | |
if (qotile.mesh.geometry) qotile.mesh.geometry.dispose(); | |
if (qotile.mesh.material) qotile.mesh.material.dispose(); | |
} | |
players = []; | |
playerProjectiles = []; | |
neutralZoneBlocks = []; | |
qotile = null; | |
createPlayers(); | |
createNeutralZone(); | |
createQotile(); | |
gameActive = true; | |
document.getElementById('gameOverScreen').style.display = 'none'; | |
updateUI(); | |
} | |
// Animation loop | |
function animate() { | |
requestAnimationFrame(animate); | |
const delta = clock.getDelta(); | |
if (gameActive) { | |
players.forEach(player => { | |
if (player && player.mesh) { | |
handlePlayerMovement(player, delta); | |
handlePlayerShooting(player, delta); | |
} | |
}); | |
updateProjectiles(delta); | |
updateQotile(delta); | |
} | |
updateUI(); | |
renderer.render(scene, camera); | |
} | |
// Event handlers | |
function onKeyDown(event) { | |
keysPressed[event.code] = true; | |
// ******** UPDATED GAMEKEYS FOR PREVENTDEFAULT ******** | |
const gameKeys = ['KeyW', 'KeyA', 'KeyS', 'KeyD', 'KeyI', 'KeyJ', 'KeyK', 'KeyL', 'KeyE', 'KeyU', 'Space']; | |
if (gameKeys.includes(event.code)) { | |
event.preventDefault(); | |
} | |
} | |
function onKeyUp(event) { | |
keysPressed[event.code] = false; | |
} | |
function onWindowResize() { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
} | |
window.onload = function() { | |
init(); | |
}; | |
</script> | |
</body> | |
</html> | |