Created
March 2, 2025 00:52
-
-
Save ashleyrudland/6b43cb4816347b1283e8dfc166a52242 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Quake 3 Arena Clone</title> | |
<link rel="preconnect" href="https://fonts.googleapis.com"> | |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;700;900&family=Exo+2:wght@400;700&family=Roboto+Mono&display=swap" rel="stylesheet"> | |
<style> | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
} | |
body { | |
overflow: hidden; | |
font-family: 'Exo 2', sans-serif; | |
color: #ffffff; | |
background-color: #111111; | |
position: relative; | |
} | |
#gameCanvas { | |
width: 100vw; | |
height: 100vh; | |
display: block; | |
} | |
#hud { | |
position: absolute; | |
bottom: 20px; | |
left: 20px; | |
z-index: 10; | |
pointer-events: none; | |
font-family: 'Roboto Mono', monospace; | |
text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.8); | |
} | |
#health, #armor { | |
margin-bottom: 10px; | |
font-size: 22px; | |
} | |
#health { | |
color: #ff3333; | |
} | |
#armor { | |
color: #3399ff; | |
} | |
#ammo { | |
color: #ffcc00; | |
font-size: 22px; | |
} | |
#scoreBoard { | |
position: absolute; | |
top: 20px; | |
right: 20px; | |
z-index: 10; | |
font-family: 'Roboto Mono', monospace; | |
text-align: right; | |
pointer-events: none; | |
text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.8); | |
} | |
#kills, #deaths { | |
margin-bottom: 5px; | |
font-size: 18px; | |
} | |
#kills { | |
color: #33ff33; | |
} | |
#deaths { | |
color: #ff3333; | |
} | |
#weapon { | |
position: absolute; | |
bottom: 20px; | |
right: 20px; | |
z-index: 10; | |
pointer-events: none; | |
text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.8); | |
font-size: 22px; | |
color: #ffffff; | |
font-family: 'Roboto Mono', monospace; | |
} | |
#crosshair { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
width: 20px; | |
height: 20px; | |
transform: translate(-50%, -50%); | |
pointer-events: none; | |
z-index: 10; | |
} | |
#crosshair::before, #crosshair::after { | |
content: ""; | |
position: absolute; | |
background-color: rgba(255, 255, 255, 0.8); | |
} | |
#crosshair::before { | |
width: 2px; | |
height: 20px; | |
left: 9px; | |
top: 0; | |
} | |
#crosshair::after { | |
width: 20px; | |
height: 2px; | |
left: 0; | |
top: 9px; | |
} | |
#homeScreen { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
display: flex; | |
flex-direction: column; | |
justify-content: center; | |
align-items: center; | |
background: linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.9)), url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMDAiIGhlaWdodD0iMzAwIj48ZmlsdGVyIGlkPSJhIiB4PSIwIiB5PSIwIj48ZmVUdXJidWxlbmNlIHR5cGU9ImZyYWN0YWxOb2lzZSIgYmFzZUZyZXF1ZW5jeT0iLjc1IiBzdGl0Y2hUaWxlcz0ic3RpdGNoIi8+PGZlQ29sb3JNYXRyaXggdHlwZT0ic2F0dXJhdGUiIHZhbHVlcz0iMCIvPjwvZmlsdGVyPjxwYXRoIGZpbHRlcj0idXJsKCNhKSIgb3BhY2l0eT0iLjA1IiBkPSJNMCAwaDMwMHYzMDBIMHoiLz48L3N2Zz4='); | |
background-size: cover; | |
z-index: 100; | |
} | |
#gameTitle { | |
font-family: 'Orbitron', sans-serif; | |
font-size: 72px; | |
font-weight: 900; | |
color: #ff6600; | |
text-shadow: 0 0 20px rgba(255, 102, 0, 0.8), 0 0 30px rgba(255, 102, 0, 0.4); | |
margin-bottom: 40px; | |
letter-spacing: 2px; | |
text-align: center; | |
animation: pulse 2s infinite; | |
} | |
@keyframes pulse { | |
0% { text-shadow: 0 0 20px rgba(255, 102, 0, 0.8), 0 0 30px rgba(255, 102, 0, 0.4); } | |
50% { text-shadow: 0 0 25px rgba(255, 102, 0, 1), 0 0 40px rgba(255, 102, 0, 0.7); } | |
100% { text-shadow: 0 0 20px rgba(255, 102, 0, 0.8), 0 0 30px rgba(255, 102, 0, 0.4); } | |
} | |
#menuOptions { | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
} | |
.menuButton { | |
font-family: 'Orbitron', sans-serif; | |
font-size: 28px; | |
color: #ffffff; | |
background-color: rgba(40, 40, 40, 0.7); | |
border: 2px solid #ff6600; | |
border-radius: 5px; | |
padding: 15px 40px; | |
margin: 10px 0; | |
cursor: pointer; | |
width: 300px; | |
text-align: center; | |
transition: all 0.3s ease; | |
text-shadow: 0 0 5px rgba(255, 255, 255, 0.5); | |
} | |
.menuButton:hover { | |
background-color: rgba(255, 102, 0, 0.4); | |
transform: scale(1.05); | |
box-shadow: 0 0 15px rgba(255, 102, 0, 0.7); | |
} | |
#pauseMenu { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
display: none; | |
flex-direction: column; | |
justify-content: center; | |
align-items: center; | |
background-color: rgba(0, 0, 0, 0.8); | |
z-index: 50; | |
} | |
#pauseTitle { | |
font-family: 'Orbitron', sans-serif; | |
font-size: 48px; | |
color: #ff6600; | |
margin-bottom: 40px; | |
text-shadow: 0 0 15px rgba(255, 102, 0, 0.6); | |
} | |
#controlsInfo { | |
position: absolute; | |
bottom: 20px; | |
width: 100%; | |
text-align: center; | |
font-family: 'Roboto Mono', monospace; | |
font-size: 16px; | |
color: #aaaaaa; | |
} | |
#messageOverlay { | |
position: absolute; | |
top: 20%; | |
left: 0; | |
width: 100%; | |
text-align: center; | |
font-family: 'Orbitron', sans-serif; | |
font-size: 42px; | |
color: #ff6600; | |
text-shadow: 0 0 10px rgba(255, 102, 0, 0.6); | |
opacity: 0; | |
z-index: 30; | |
transition: opacity 0.5s ease; | |
pointer-events: none; | |
} | |
#loadingScreen { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
display: flex; | |
flex-direction: column; | |
justify-content: center; | |
align-items: center; | |
background-color: #111111; | |
z-index: 200; | |
} | |
#loadingTitle { | |
font-family: 'Orbitron', sans-serif; | |
font-size: 36px; | |
color: #ff6600; | |
margin-bottom: 40px; | |
} | |
#loadingBar { | |
width: 400px; | |
height: 20px; | |
background-color: #333333; | |
border-radius: 10px; | |
overflow: hidden; | |
} | |
#loadingProgress { | |
height: 100%; | |
width: 0%; | |
background-color: #ff6600; | |
transition: width 0.3s ease; | |
} | |
#version { | |
position: absolute; | |
bottom: 10px; | |
left: 10px; | |
font-family: 'Roboto Mono', monospace; | |
font-size: 12px; | |
color: #666666; | |
} | |
#weaponModel { | |
position: absolute; | |
bottom: 0; | |
right: 0; | |
width: 33%; | |
height: 33%; | |
z-index: 5; | |
pointer-events: none; | |
} | |
.playerDamage { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background-color: rgba(255, 0, 0, 0.3); | |
pointer-events: none; | |
opacity: 0; | |
z-index: 8; | |
transition: opacity 0.2s ease; | |
} | |
#minimap { | |
position: absolute; | |
top: 20px; | |
left: 20px; | |
width: 200px; | |
height: 200px; | |
background-color: rgba(0, 0, 0, 0.5); | |
border: 2px solid #ff6600; | |
border-radius: 5px; | |
z-index: 10; | |
overflow: hidden; | |
} | |
.minimapDot { | |
position: absolute; | |
width: 8px; | |
height: 8px; | |
border-radius: 50%; | |
transform: translate(-50%, -50%); | |
} | |
.playerDot { | |
background-color: #33ff33; | |
} | |
.botDot { | |
background-color: #ff3333; | |
} | |
#botList { | |
position: absolute; | |
top: 240px; | |
left: 20px; | |
width: 200px; | |
background-color: rgba(0, 0, 0, 0.5); | |
border: 2px solid #ff6600; | |
border-radius: 5px; | |
z-index: 10; | |
padding: 10px; | |
font-family: 'Roboto Mono', monospace; | |
font-size: 14px; | |
} | |
.botEntry { | |
display: flex; | |
justify-content: space-between; | |
margin: 5px 0; | |
} | |
.botName { | |
color: #ff3333; | |
} | |
.botHealth { | |
color: #33ff33; | |
} | |
#timerDisplay { | |
position: absolute; | |
top: 20px; | |
left: 50%; | |
transform: translateX(-50%); | |
font-family: 'Roboto Mono', monospace; | |
font-size: 24px; | |
color: #ffffff; | |
text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.8); | |
z-index: 10; | |
} | |
#deathMessage { | |
position: absolute; | |
top: 40%; | |
left: 0; | |
width: 100%; | |
text-align: center; | |
font-family: 'Orbitron', sans-serif; | |
font-size: 48px; | |
color: #ff3333; | |
text-shadow: 0 0 15px rgba(255, 51, 51, 0.8); | |
opacity: 0; | |
z-index: 20; | |
transition: opacity 0.5s ease; | |
pointer-events: none; | |
} | |
#hitMarker { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%) rotate(45deg); | |
width: 20px; | |
height: 20px; | |
opacity: 0; | |
z-index: 15; | |
pointer-events: none; | |
transition: opacity 0.1s ease; | |
} | |
#hitMarker::before, #hitMarker::after { | |
content: ""; | |
position: absolute; | |
background-color: rgba(255, 255, 255, 0.8); | |
} | |
#hitMarker::before { | |
width: 2px; | |
height: 20px; | |
left: 9px; | |
top: 0; | |
} | |
#hitMarker::after { | |
width: 20px; | |
height: 2px; | |
left: 0; | |
top: 9px; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="loadingScreen"> | |
<h1 id="loadingTitle">LOADING ARENA</h1> | |
<div id="loadingBar"> | |
<div id="loadingProgress"></div> | |
</div> | |
</div> | |
<div id="homeScreen"> | |
<h1 id="gameTitle">QUAKE ARENA</h1> | |
<div id="menuOptions"> | |
<button id="playButton" class="menuButton">PLAY GAME</button> | |
<button id="resumeButton" class="menuButton" style="display: none;">RESUME GAME</button> | |
<button id="settingsButton" class="menuButton">SETTINGS</button> | |
<button id="controlsButton" class="menuButton">CONTROLS</button> | |
</div> | |
<div id="controlsInfo"> | |
WASD - Movement | MOUSE - Look | LEFT CLICK - Shoot | SPACE - Jump | 1-4 - Weapons | ESC - Pause | |
</div> | |
<div id="version">v1.0.0</div> | |
</div> | |
<div id="pauseMenu"> | |
<h2 id="pauseTitle">GAME PAUSED</h2> | |
<div id="menuOptions"> | |
<button id="resumeGameButton" class="menuButton">RESUME GAME</button> | |
<button id="homeButton" class="menuButton">HOME SCREEN</button> | |
<button id="pauseSettingsButton" class="menuButton">SETTINGS</button> | |
</div> | |
</div> | |
<canvas id="gameCanvas"></canvas> | |
<div id="hud"> | |
<div id="health">HEALTH: 100</div> | |
<div id="armor">ARMOR: 0</div> | |
<div id="ammo">AMMO: 50</div> | |
</div> | |
<div id="scoreBoard"> | |
<div id="kills">KILLS: 0</div> | |
<div id="deaths">DEATHS: 0</div> | |
</div> | |
<div id="weapon">MACHINE GUN</div> | |
<div id="crosshair"></div> | |
<div id="minimap"></div> | |
<div id="botList"></div> | |
<div id="timerDisplay">10:00</div> | |
<div id="messageOverlay"></div> | |
<div id="deathMessage"></div> | |
<div id="hitMarker"></div> | |
<div class="playerDamage"></div> | |
<div id="weaponModel"></div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script> | |
<script src="https://unpkg.com/[email protected]/build/index.js"></script> | |
<script> | |
// Initialize game variables | |
let gameStarted = false; | |
let gamePaused = false; | |
let gameActive = false; | |
let audioInitialized = false; | |
// Main game class | |
class QuakeArenaGame { | |
constructor() { | |
this.scene = null; | |
this.camera = null; | |
this.renderer = null; | |
this.player = null; | |
this.bots = []; | |
this.weapons = []; | |
this.projectiles = []; | |
this.pickups = []; | |
this.level = null; | |
this.clock = new THREE.Clock(); | |
this.deltaTime = 0; | |
this.elapsedTime = 0; | |
this.matchTime = 600; // 10 minutes in seconds | |
this.remainingTime = this.matchTime; | |
this.respawnTime = 3; | |
this.playerScore = { | |
kills: 0, | |
deaths: 0 | |
}; | |
this.botCount = 5; | |
this.botNames = [ | |
"FragBot", "DeathDealer", "Terminator", | |
"Annihilator", "RocketMan", "SniperX", | |
"DoomBringer", "Shredder", "HardCore", | |
"BloodHound", "VenomShot", "ThunderStrike" | |
]; | |
this.init(); | |
} | |
init() { | |
// Create THREE.js scene | |
this.scene = new THREE.Scene(); | |
this.scene.background = new THREE.Color(0x222222); | |
this.scene.fog = new THREE.FogExp2(0x222222, 0.01); | |
// Setup camera | |
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
// Setup renderer | |
this.renderer = new THREE.WebGLRenderer({ | |
canvas: document.getElementById('gameCanvas'), | |
antialias: true | |
}); | |
this.renderer.setSize(window.innerWidth, window.innerHeight); | |
this.renderer.shadowMap.enabled = true; | |
// Add lights | |
this.setupLights(); | |
// Create arena level | |
this.level = new Level(this.scene); | |
// Create player | |
this.player = new Player(this.camera, this.scene); | |
// Create weapons | |
this.createWeapons(); | |
// Create pickups | |
this.createPickups(); | |
// Handle window resize | |
window.addEventListener('resize', () => { | |
this.camera.aspect = window.innerWidth / window.innerHeight; | |
this.camera.updateProjectionMatrix(); | |
this.renderer.setSize(window.innerWidth, window.innerHeight); | |
}); | |
// Initialize controls | |
this.initControls(); | |
// Set up minimap | |
this.setupMinimap(); | |
// Start loading process | |
this.startLoading(); | |
} | |
setupLights() { | |
// Main directional light (sun) | |
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); | |
// Ambient light | |
const ambientLight = new THREE.AmbientLight(0x404040, 0.6); | |
this.scene.add(ambientLight); | |
// Add point lights around the arena for better illumination | |
const pointLightPositions = [ | |
{ x: 20, y: 10, z: 20, color: 0xffaa44, intensity: 0.8 }, | |
{ x: -20, y: 10, z: 20, color: 0x44aaff, intensity: 0.8 }, | |
{ x: 20, y: 10, z: -20, color: 0x44ffaa, intensity: 0.8 }, | |
{ x: -20, y: 10, z: -20, color: 0xaa44ff, intensity: 0.8 } | |
]; | |
pointLightPositions.forEach(pos => { | |
const pointLight = new THREE.PointLight(pos.color, pos.intensity, 50); | |
pointLight.position.set(pos.x, pos.y, pos.z); | |
pointLight.castShadow = true; | |
this.scene.add(pointLight); | |
}); | |
} | |
createWeapons() { | |
// Define weapons | |
const weaponData = [ | |
{ | |
name: "MACHINE GUN", | |
damage: 10, | |
fireRate: 0.1, | |
ammo: 100, | |
model: null, | |
projectileSpeed: 100, | |
projectileSize: 0.1, | |
projectileColor: 0xffff00, | |
soundEffect: "laser" | |
}, | |
{ | |
name: "SHOTGUN", | |
damage: 60, | |
fireRate: 0.9, | |
ammo: 25, | |
model: null, | |
pellets: 8, | |
spread: 0.1, | |
projectileSpeed: 120, | |
projectileSize: 0.08, | |
projectileColor: 0xff6600, | |
soundEffect: "explosion" | |
}, | |
{ | |
name: "ROCKET LAUNCHER", | |
damage: 100, | |
fireRate: 1.0, | |
ammo: 10, | |
model: null, | |
projectileSpeed: 40, | |
projectileSize: 0.3, | |
projectileColor: 0xff0000, | |
splashRadius: 6, | |
splashDamage: 60, | |
soundEffect: "explosion" | |
}, | |
{ | |
name: "RAILGUN", | |
damage: 100, | |
fireRate: 1.5, | |
ammo: 10, | |
model: null, | |
isHitscan: true, | |
projectileColor: 0x00ffff, | |
soundEffect: "synth" | |
} | |
]; | |
// Create weapon instances | |
this.weapons = weaponData.map(data => new Weapon(data, this.scene)); | |
// Assign first weapon to player | |
this.player.currentWeapon = this.weapons[0]; | |
this.updateWeaponHUD(); | |
} | |
createPickups() { | |
// Define pickup types and positions | |
const pickupTypes = [ | |
{ type: 'health', model: 'healthPack', respawnTime: 15, value: 25 }, | |
{ type: 'armor', model: 'armorShard', respawnTime: 20, value: 25 }, | |
{ type: 'ammo', model: 'ammoBox', respawnTime: 15, value: 20 }, | |
{ type: 'megaHealth', model: 'megaHealth', respawnTime: 30, value: 100 }, | |
{ type: 'heavyArmor', model: 'heavyArmor', respawnTime: 30, value: 100 } | |
]; | |
// Pickup positions throughout the arena | |
const pickupPositions = [ | |
{ x: 20, y: 1, z: 0 }, | |
{ x: -20, y: 1, z: 0 }, | |
{ x: 0, y: 1, z: 20 }, | |
{ x: 0, y: 1, z: -20 }, | |
{ x: 15, y: 1, z: 15 }, | |
{ x: -15, y: 1, z: 15 }, | |
{ x: 15, y: 1, z: -15 }, | |
{ x: -15, y: 1, z: -15 }, | |
{ x: 0, y: 8, z: 0 }, // Central platform | |
{ x: 25, y: 1, z: 25 }, | |
{ x: -25, y: 1, z: 25 }, | |
{ x: 25, y: 1, z: -25 }, | |
{ x: -25, y: 1, z: -25 } | |
]; | |
// Create pickup instances at various positions | |
for (let i = 0; i < pickupPositions.length; i++) { | |
const pos = pickupPositions[i]; | |
const pickupType = pickupTypes[i % pickupTypes.length]; | |
this.pickups.push(new Pickup( | |
this.scene, | |
pickupType.type, | |
pickupType.model, | |
new THREE.Vector3(pos.x, pos.y, pos.z), | |
pickupType.respawnTime, | |
pickupType.value | |
)); | |
} | |
} | |
startLoading() { | |
const loadingScreen = document.getElementById('loadingScreen'); | |
const loadingProgress = document.getElementById('loadingProgress'); | |
let progress = 0; | |
// Simulate loading progress | |
const loadingInterval = setInterval(() => { | |
progress += Math.random() * 10; | |
if (progress >= 100) { | |
progress = 100; | |
clearInterval(loadingInterval); | |
// Hide loading screen when done | |
setTimeout(() => { | |
loadingScreen.style.display = 'none'; | |
}, 500); | |
} | |
loadingProgress.style.width = `${progress}%`; | |
}, 200); | |
} | |
initControls() { | |
// Add event listeners for keyboard and mouse | |
document.addEventListener('keydown', this.handleKeyDown.bind(this)); | |
document.addEventListener('keyup', this.handleKeyUp.bind(this)); | |
document.addEventListener('mousedown', this.handleMouseDown.bind(this)); | |
document.addEventListener('mouseup', this.handleMouseUp.bind(this)); | |
document.addEventListener('mousemove', this.handleMouseMove.bind(this)); | |
document.addEventListener('pointerlockchange', this.handlePointerLockChange.bind(this)); | |
// Set up UI buttons | |
document.getElementById('playButton').addEventListener('click', this.startGame.bind(this)); | |
document.getElementById('resumeButton').addEventListener('click', this.resumeGame.bind(this)); | |
document.getElementById('settingsButton').addEventListener('click', () => this.showMessage('Settings not implemented yet')); | |
document.getElementById('controlsButton').addEventListener('click', () => this.showMessage('Controls: WASD to move, Mouse to aim, Click to shoot, Space to jump, 1-4 to switch weapons, ESC to pause')); | |
document.getElementById('resumeGameButton').addEventListener('click', this.resumeGame.bind(this)); | |
document.getElementById('homeButton').addEventListener('click', this.returnToHome.bind(this)); | |
document.getElementById('pauseSettingsButton').addEventListener('click', () => this.showMessage('Settings not implemented yet')); | |
// Initialize sounds with click event listener | |
document.addEventListener('touchstart', this.initializeAudio.bind(this), { once: true }); | |
document.addEventListener('click', this.initializeAudio.bind(this), { once: true }); | |
} | |
initializeAudio() { | |
if (!audioInitialized) { | |
sss.init(42); | |
sss.startAudio(); | |
// Play background music | |
sss.playMml([ | |
"@synth@s308454596 v50 l16 o4 r4b4 >c+erer8.<b b2 >c+2 <b2 >c+ec+<ar>c+r<a f+g+af+rf+er e2", | |
"@synth@s771118616 v35 l4 o4 f+f+ f+1 >c+ <g+ f+f+ eg+ ab b2", | |
"@synth@s848125671 v40 l4 o4 d+16d+16f+16e16e16e16e16<b16 >ee b8.b16r8>f+8 c+c+ <b>f+ <aa a2 bb", | |
"@explosion@d@s364411560 v40 l16 o4 cr8.cr8. cr8.cr8. cr8.cr8. cr8.cr8. cr8.cr8. cr8.cr8. cr8.cr8. cr8.cr8.", | |
"@explosion@d@s152275772 v40 l16 o4 r8crcrcr8. cccrcr8. crcrcr8. crcrcr8. crcrcr8. crcrcr8. crcrcr8. crcrcr", | |
"@hit@d@s234851483 v50 l16 o4 rcr4^16c rcr4. ccr4^16c rcr4.^16 cr4^16c rcr4.^16 cr4^16c rcr4.", | |
]); | |
audioInitialized = true; | |
} | |
} | |
startGame() { | |
if (!audioInitialized) { | |
this.initializeAudio(); | |
} | |
// Reset game state | |
this.player.reset(); | |
this.projectiles = []; | |
this.remainingTime = this.matchTime; | |
this.playerScore.kills = 0; | |
this.playerScore.deaths = 0; | |
// Update HUD | |
this.updateHealthHUD(); | |
this.updateArmorHUD(); | |
this.updateWeaponHUD(); | |
this.updateAmmoHUD(); | |
this.updateScoreHUD(); | |
// Create bots | |
this.createBots(); | |
// Hide home screen | |
document.getElementById('homeScreen').style.display = 'none'; | |
// Lock pointer | |
document.getElementById('gameCanvas').requestPointerLock(); | |
// Start game loop | |
if (!gameStarted) { | |
gameStarted = true; | |
this.animate(); | |
} | |
gameActive = true; | |
gamePaused = false; | |
// Play start sound | |
sss.playSoundEffect("powerUp"); | |
// Show game start message | |
this.showMessage("FIGHT!"); | |
} | |
resumeGame() { | |
// Hide screens | |
document.getElementById('homeScreen').style.display = 'none'; | |
document.getElementById('pauseMenu').style.display = 'none'; | |
// Lock pointer | |
document.getElementById('gameCanvas').requestPointerLock(); | |
gamePaused = false; | |
gameActive = true; | |
// Resume clock | |
this.clock.start(); | |
// Play resume sound | |
sss.playSoundEffect("select"); | |
} | |
pauseGame() { | |
if (!gameActive) return; | |
gamePaused = true; | |
// Pause clock | |
this.clock.stop(); | |
// Show pause menu | |
document.getElementById('pauseMenu').style.display = 'flex'; | |
// Unlock pointer | |
document.exitPointerLock(); | |
// Play pause sound | |
sss.playSoundEffect("select"); | |
} | |
returnToHome() { | |
// Show home screen with resume option | |
document.getElementById('pauseMenu').style.display = 'none'; | |
document.getElementById('homeScreen').style.display = 'flex'; | |
document.getElementById('playButton').style.display = 'none'; | |
document.getElementById('resumeButton').style.display = 'block'; | |
// Play menu sound | |
sss.playSoundEffect("select"); | |
} | |
createBots() { | |
// Clear existing bots | |
this.bots.forEach(bot => this.scene.remove(bot.mesh)); | |
this.bots = []; | |
// Create new bots | |
for (let i = 0; i < this.botCount; i++) { | |
// Choose random position for bot | |
const spawnPoints = this.level.spawnPoints; | |
const spawnPoint = spawnPoints[Math.floor(Math.random() * spawnPoints.length)]; | |
const botName = this.botNames[Math.floor(Math.random() * this.botNames.length)]; | |
// Create bot with random weapon | |
const bot = new Bot( | |
this.scene, | |
spawnPoint.clone(), | |
botName, | |
this.weapons[Math.floor(Math.random() * this.weapons.length)] | |
); | |
this.bots.push(bot); | |
} | |
// Update bot list display | |
this.updateBotList(); | |
} | |
setupMinimap() { | |
this.minimap = document.getElementById('minimap'); | |
this.minimapSize = 200; | |
this.minimapScale = 0.1; // Scale factor for minimap | |
} | |
updateMinimap() { | |
// Clear previous minimap elements | |
this.minimap.innerHTML = ''; | |
// Add player dot | |
const playerPos = this.player.position.clone(); | |
const playerDot = document.createElement('div'); | |
playerDot.className = 'minimapDot playerDot'; | |
playerDot.style.left = `${(playerPos.x * this.minimapScale + this.minimapSize / 2)}px`; | |
playerDot.style.top = `${(playerPos.z * this.minimapScale + this.minimapSize / 2)}px`; | |
this.minimap.appendChild(playerDot); | |
// Add bot dots | |
this.bots.forEach(bot => { | |
if (bot.health <= 0) return; // Don't show dead bots | |
const botPos = bot.position; | |
const botDot = document.createElement('div'); | |
botDot.className = 'minimapDot botDot'; | |
botDot.style.left = `${(botPos.x * this.minimapScale + this.minimapSize / 2)}px`; | |
botDot.style.top = `${(botPos.z * this.minimapScale + this.minimapSize / 2)}px`; | |
this.minimap.appendChild(botDot); | |
}); | |
} | |
updateBotList() { | |
const botList = document.getElementById('botList'); | |
botList.innerHTML = ''; | |
this.bots.forEach(bot => { | |
const botEntry = document.createElement('div'); | |
botEntry.className = 'botEntry'; | |
const botName = document.createElement('span'); | |
botName.className = 'botName'; | |
botName.textContent = bot.name; | |
const botHealth = document.createElement('span'); | |
botHealth.className = 'botHealth'; | |
botHealth.textContent = Math.max(0, bot.health); | |
botEntry.appendChild(botName); | |
botEntry.appendChild(botHealth); | |
botList.appendChild(botEntry); | |
}); | |
} | |
handleKeyDown(e) { | |
if (e.key === 'Escape') { | |
if (gameActive) { | |
if (gamePaused) { | |
this.resumeGame(); | |
} else { | |
this.pauseGame(); | |
} | |
} | |
return; | |
} | |
if (gamePaused || !gameActive) return; | |
this.player.handleKeyDown(e); | |
// Handle weapon switching | |
if (e.key >= '1' && e.key <= '4') { | |
const weaponIndex = parseInt(e.key) - 1; | |
if (weaponIndex < this.weapons.length) { | |
this.player.currentWeapon = this.weapons[weaponIndex]; | |
this.updateWeaponHUD(); | |
sss.playSoundEffect("select"); | |
} | |
} | |
} | |
handleKeyUp(e) { | |
if (gamePaused || !gameActive) return; | |
this.player.handleKeyUp(e); | |
} | |
handleMouseDown(e) { | |
if (gamePaused || !gameActive) return; | |
if (e.button === 0) { // Left mouse button | |
this.player.isShooting = true; | |
} | |
} | |
handleMouseUp(e) { | |
if (gamePaused || !gameActive) return; | |
if (e.button === 0) { // Left mouse button | |
this.player.isShooting = false; | |
} | |
} | |
handleMouseMove(e) { | |
if (gamePaused || !gameActive) return; | |
if (document.pointerLockElement === document.getElementById('gameCanvas')) { | |
this.player.handleMouseMove(e); | |
} | |
} | |
handlePointerLockChange() { | |
if (document.pointerLockElement !== document.getElementById('gameCanvas')) { | |
// Pointer is unlocked but game is active - pause the game | |
if (gameActive && !gamePaused) { | |
this.pauseGame(); | |
} | |
} | |
} | |
updateHealthHUD() { | |
document.getElementById('health').textContent = `HEALTH: ${this.player.health}`; | |
} | |
updateArmorHUD() { | |
document.getElementById('armor').textContent = `ARMOR: ${this.player.armor}`; | |
} | |
updateWeaponHUD() { | |
document.getElementById('weapon').textContent = this.player.currentWeapon.name; | |
} | |
updateAmmoHUD() { | |
document.getElementById('ammo').textContent = `AMMO: ${this.player.currentWeapon.ammo}`; | |
} | |
updateScoreHUD() { | |
document.getElementById('kills').textContent = `KILLS: ${this.playerScore.kills}`; | |
document.getElementById('deaths').textContent = `DEATHS: ${this.playerScore.deaths}`; | |
} | |
updateTimerHUD() { | |
const minutes = Math.floor(this.remainingTime / 60); | |
const seconds = Math.floor(this.remainingTime % 60); | |
document.getElementById('timerDisplay').textContent = | |
`${minutes}:${seconds < 10 ? '0' : ''}${seconds}`; | |
} | |
showMessage(message, duration = 2000) { | |
const messageOverlay = document.getElementById('messageOverlay'); | |
messageOverlay.textContent = message; | |
messageOverlay.style.opacity = 1; | |
setTimeout(() => { | |
messageOverlay.style.opacity = 0; | |
}, duration); | |
} | |
showHitMarker() { | |
const hitMarker = document.getElementById('hitMarker'); | |
hitMarker.style.opacity = 1; | |
setTimeout(() => { | |
hitMarker.style.opacity = 0; | |
}, 100); | |
} | |
showPlayerDamage() { | |
const damageOverlay = document.querySelector('.playerDamage'); | |
damageOverlay.style.opacity = 1; | |
setTimeout(() => { | |
damageOverlay.style.opacity = 0; | |
}, 200); | |
} | |
showDeathMessage() { | |
const deathMessage = document.getElementById('deathMessage'); | |
deathMessage.textContent = "YOU DIED"; | |
deathMessage.style.opacity = 1; | |
setTimeout(() => { | |
deathMessage.style.opacity = 0; | |
}, 2000); | |
} | |
fireProjectile(shooter, weapon) { | |
if (weapon.ammo <= 0) { | |
// Play empty weapon sound | |
sss.playSoundEffect("click"); | |
return; | |
} | |
// Play weapon sound | |
sss.playSoundEffect(weapon.soundEffect); | |
// Decrease ammo | |
weapon.ammo--; | |
if (shooter === this.player) { | |
this.updateAmmoHUD(); | |
} | |
if (weapon.isHitscan) { | |
// Handle railgun/hitscan weapons | |
this.handleHitscanWeapon(shooter, weapon); | |
} else if (weapon.pellets) { | |
// Handle shotgun with multiple pellets | |
for (let i = 0; i < weapon.pellets; i++) { | |
this.createProjectile(shooter, weapon, true); | |
} | |
} else { | |
// Handle normal projectile weapons | |
this.createProjectile(shooter, weapon, false); | |
} | |
} | |
handleHitscanWeapon(shooter, weapon) { | |
const raycaster = new THREE.Raycaster(); | |
const direction = new THREE.Vector3(); | |
if (shooter === this.player) { | |
// Player shooting | |
direction.set(0, 0, -1).unproject(this.camera).sub(this.camera.position).normalize(); | |
raycaster.set(this.camera.position, direction); | |
} else { | |
// Bot shooting | |
direction.copy(shooter.direction).normalize(); | |
raycaster.set(shooter.position, direction); | |
} | |
// Visual effect for railgun (beam) | |
const beamGeometry = new THREE.CylinderGeometry(0.05, 0.05, 100, 8); | |
beamGeometry.rotateX(Math.PI / 2); | |
const beamMaterial = new THREE.MeshBasicMaterial({ | |
color: weapon.projectileColor, | |
transparent: true, | |
opacity: 0.8 | |
}); | |
const beam = new THREE.Mesh(beamGeometry, beamMaterial); | |
beam.position.copy(shooter.position); | |
if (shooter === this.player) { | |
beam.position.copy(this.camera.position); | |
beam.position.add(direction.multiplyScalar(2)); // Move forward a bit | |
} | |
beam.lookAt(beam.position.clone().add(direction.multiplyScalar(10))); | |
this.scene.add(beam); | |
// Remove beam after short duration | |
setTimeout(() => { | |
this.scene.remove(beam); | |
}, 100); | |
// Check for hits | |
const intersects = raycaster.intersectObjects(this.scene.children, true); | |
for (const intersect of intersects) { | |
// Skip self hit | |
if (shooter === this.player && intersect.object === this.player.mesh) continue; | |
// Check if hit a bot | |
const hitBot = this.bots.find(bot => bot.mesh === intersect.object || bot.mesh.children.includes(intersect.object)); | |
if (hitBot && hitBot.health > 0) { | |
hitBot.takeDamage(weapon.damage, shooter); | |
if (shooter === this.player) { | |
this.showHitMarker(); | |
if (hitBot.health <= 0) { | |
this.playerScore.kills++; | |
this.updateScoreHUD(); | |
this.showMessage(`Killed ${hitBot.name}!`); | |
sss.playSoundEffect("lucky"); | |
} | |
} | |
// Create hit effect | |
this.createHitEffect(intersect.point); | |
break; | |
} | |
// Check if hit player | |
if (shooter !== this.player && intersect.object === this.player.mesh) { | |
this.player.takeDamage(weapon.damage, shooter); | |
this.updateHealthHUD(); | |
this.updateArmorHUD(); | |
this.showPlayerDamage(); | |
// Create hit effect | |
this.createHitEffect(intersect.point); | |
break; | |
} | |
// Hit environment | |
if (intersect.object.isMesh && !intersect.object.isProjectile) { | |
// Create spark effect | |
this.createHitEffect(intersect.point); | |
break; | |
} | |
} | |
} | |
createProjectile(shooter, weapon, isSpread) { | |
// Create projectile mesh | |
const geometry = new THREE.SphereGeometry(weapon.projectileSize, 8, 8); | |
const material = new THREE.MeshBasicMaterial({ color: weapon.projectileColor }); | |
const projectile = new THREE.Mesh(geometry, material); | |
// Flag for collision detection | |
projectile.isProjectile = true; | |
// Starting position | |
if (shooter === this.player) { | |
// Player projectile starts at camera position | |
projectile.position.copy(this.camera.position); | |
} else { | |
// Bot projectile starts at bot position | |
projectile.position.copy(shooter.position.clone().add(new THREE.Vector3(0, 1.5, 0))); | |
} | |
// Calculate direction | |
let direction; | |
if (shooter === this.player) { | |
direction = new THREE.Vector3(0, 0, -1); | |
direction.unproject(this.camera).sub(this.camera.position).normalize(); | |
} else { | |
direction = shooter.direction.clone().normalize(); | |
} | |
// Add spread if needed (shotgun) | |
if (isSpread) { | |
const spread = weapon.spread; | |
direction.x += (Math.random() - 0.5) * spread; | |
direction.y += (Math.random() - 0.5) * spread; | |
direction.z += (Math.random() - 0.5) * spread; | |
direction.normalize(); | |
} | |
this.scene.add(projectile); | |
// Add to projectiles array with metadata | |
this.projectiles.push({ | |
mesh: projectile, | |
direction: direction, | |
speed: weapon.projectileSpeed, | |
damage: weapon.damage, | |
shooter: shooter, | |
age: 0, | |
maxAge: 5, // seconds before despawning | |
splashRadius: weapon.splashRadius || 0, | |
splashDamage: weapon.splashDamage || 0 | |
}); | |
} | |
createHitEffect(position) { | |
// Create particle effect for hit | |
const particleCount = 15; | |
const particles = new THREE.Group(); | |
for (let i = 0; i < particleCount; i++) { | |
const size = Math.random() * 0.1 + 0.05; | |
const geometry = new THREE.SphereGeometry(size, 4, 4); | |
const material = new THREE.MeshBasicMaterial({ | |
color: 0xffff00, | |
transparent: true, | |
opacity: 0.8 | |
}); | |
const particle = new THREE.Mesh(geometry, material); | |
particle.position.copy(position); | |
// Random velocity | |
particle.velocity = new THREE.Vector3( | |
(Math.random() - 0.5) * 5, | |
(Math.random() - 0.5) * 5, | |
(Math.random() - 0.5) * 5 | |
); | |
particle.life = 0.5; // Lifetime in seconds | |
particles.add(particle); | |
} | |
this.scene.add(particles); | |
// Remove particles after animation | |
setTimeout(() => { | |
this.scene.remove(particles); | |
}, 500); | |
} | |
createExplosion(position, radius) { | |
// Create explosion mesh | |
const explosionGeometry = new THREE.SphereGeometry(radius, 16, 16); | |
const explosionMaterial = new THREE.MeshBasicMaterial({ | |
color: 0xff6600, | |
transparent: true, | |
opacity: 0.8 | |
}); | |
const explosion = new THREE.Mesh(explosionGeometry, explosionMaterial); | |
explosion.position.copy(position); | |
this.scene.add(explosion); | |
// Create additional particles | |
const particleCount = 30; | |
const particles = new THREE.Group(); | |
for (let i = 0; i < particleCount; i++) { | |
const size = Math.random() * 0.2 + 0.1; | |
const geometry = new THREE.SphereGeometry(size, 4, 4); | |
const material = new THREE.MeshBasicMaterial({ | |
color: Math.random() > 0.5 ? 0xff6600 : 0xffcc00, | |
transparent: true, | |
opacity: 0.8 | |
}); | |
const particle = new THREE.Mesh(geometry, material); | |
particle.position.copy(position); | |
// Random velocity | |
particle.velocity = new THREE.Vector3( | |
(Math.random() - 0.5) * 10, | |
(Math.random() - 0.5) * 10 + 5, // Upward bias | |
(Math.random() - 0.5) * 10 | |
); | |
particle.life = 1.0; // Lifetime in seconds | |
particles.add(particle); | |
} | |
this.scene.add(particles); | |
// Animate explosion | |
let scale = 1; | |
const expandInterval = setInterval(() => { | |
scale *= 0.9; | |
explosion.material.opacity *= 0.9; | |
explosion.scale.set(scale, scale, scale); | |
if (explosion.material.opacity < 0.05) { | |
clearInterval(expandInterval); | |
this.scene.remove(explosion); | |
} | |
}, 50); | |
// Remove particles after animation | |
setTimeout(() => { | |
this.scene.remove(particles); | |
}, 1000); | |
// Play explosion sound | |
sss.playSoundEffect("explosion"); | |
} | |
updateProjectiles(deltaTime) { | |
for (let i = this.projectiles.length - 1; i >= 0; i--) { | |
const projectile = this.projectiles[i]; | |
// Update age | |
projectile.age += deltaTime; | |
if (projectile.age > projectile.maxAge) { | |
this.scene.remove(projectile.mesh); | |
this.projectiles.splice(i, 1); | |
continue; | |
} | |
// Move projectile | |
const moveAmount = projectile.speed * deltaTime; | |
projectile.mesh.position.add( | |
projectile.direction.clone().multiplyScalar(moveAmount) | |
); | |
// Check for collisions | |
this.checkProjectileCollisions(projectile, i); | |
} | |
} | |
checkProjectileCollisions(projectile, index) { | |
// Create a raycaster for collision detection | |
const raycaster = new THREE.Raycaster( | |
projectile.mesh.position.clone().sub(projectile.direction.clone().multiplyScalar(0.2)), | |
projectile.direction, | |
0, | |
0.4 | |
); | |
// Check intersections with objects in scene | |
const intersects = raycaster.intersectObjects(this.scene.children, true); | |
for (const intersect of intersects) { | |
// Skip self intersections and other projectiles | |
if (intersect.object === projectile.mesh || intersect.object.isProjectile) continue; | |
// Skip shooter mesh (to prevent self-hits) | |
if (projectile.shooter === this.player && intersect.object === this.player.mesh) continue; | |
// Check if hit a bot | |
const hitBot = this.bots.find(bot => | |
bot.mesh === intersect.object || (bot.mesh && bot.mesh.children.includes(intersect.object)) | |
); | |
if (hitBot && hitBot.health > 0 && hitBot !== projectile.shooter) { | |
// Deal damage to bot | |
hitBot.takeDamage(projectile.damage, projectile.shooter); | |
// Show hit marker if player hit a bot | |
if (projectile.shooter === this.player) { | |
this.showHitMarker(); | |
// Check if kill | |
if (hitBot.health <= 0) { | |
this.playerScore.kills++; | |
this.updateScoreHUD(); | |
this.showMessage(`Killed ${hitBot.name}!`); | |
sss.playSoundEffect("lucky"); | |
} | |
} | |
// Remove projectile | |
this.scene.remove(projectile.mesh); | |
this.projectiles.splice(index, 1); | |
// Create hit effect | |
this.createHitEffect(intersect.point); | |
// Check if splash damage (rocket launcher) | |
if (projectile.splashRadius > 0) { | |
this.createExplosion(intersect.point, projectile.splashRadius); | |
this.applyExplosionDamage(intersect.point, projectile); | |
} | |
return; | |
} | |
// Check if hit player | |
if (intersect.object === this.player.mesh && projectile.shooter !== this.player) { | |
// Deal damage to player | |
this.player.takeDamage(projectile.damage, projectile.shooter); | |
this.updateHealthHUD(); | |
this.updateArmorHUD(); | |
this.showPlayerDamage(); | |
// Remove projectile | |
this.scene.remove(projectile.mesh); | |
this.projectiles.splice(index, 1); | |
// Create hit effect | |
this.createHitEffect(intersect.point); | |
// Check if splash damage (rocket launcher) | |
if (projectile.splashRadius > 0) { | |
this.createExplosion(intersect.point, projectile.splashRadius); | |
this.applyExplosionDamage(intersect.point, projectile); | |
} | |
return; | |
} | |
// Hit environment | |
if (intersect.object.isMesh) { | |
// Remove projectile | |
this.scene.remove(projectile.mesh); | |
this.projectiles.splice(index, 1); | |
// Create hit effect on wall/floor | |
this.createHitEffect(intersect.point); | |
// Check if splash damage (rocket launcher) | |
if (projectile.splashRadius > 0) { | |
this.createExplosion(intersect.point, projectile.splashRadius); | |
this.applyExplosionDamage(intersect.point, projectile); | |
} | |
return; | |
} | |
} | |
} | |
applyExplosionDamage(position, projectile) { | |
// Check distance to player | |
const distanceToPlayer = position.distanceTo(this.player.position); | |
if (distanceToPlayer < projectile.splashRadius && projectile.shooter !== this.player) { | |
// Calculate damage based on distance (more damage closer to explosion) | |
const damageMultiplier = 1 - (distanceToPlayer / projectile.splashRadius); | |
const damage = Math.floor(projectile.splashDamage * damageMultiplier); | |
// Apply damage to player | |
this.player.takeDamage(damage, projectile.shooter); | |
this.updateHealthHUD(); | |
this.updateArmorHUD(); | |
this.showPlayerDamage(); | |
} | |
// Check distance to bots | |
this.bots.forEach(bot => { | |
if (bot.health <= 0) return; | |
const distanceToBot = position.distanceTo(bot.position); | |
if (distanceToBot < projectile.splashRadius && bot !== projectile.shooter) { | |
// Calculate damage based on distance | |
const damageMultiplier = 1 - (distanceToBot / projectile.splashRadius); | |
const damage = Math.floor(projectile.splashDamage * damageMultiplier); | |
// Apply damage to bot | |
bot.takeDamage(damage, projectile.shooter); | |
// Check if player's splash damage killed bot | |
if (projectile.shooter === this.player && bot.health <= 0) { | |
this.playerScore.kills++; | |
this.updateScoreHUD(); | |
this.showMessage(`Killed ${bot.name}!`); | |
sss.playSoundEffect("lucky"); | |
} | |
} | |
}); | |
} | |
checkPickupCollisions() { | |
// Check if player is colliding with any pickups | |
this.pickups.forEach(pickup => { | |
if (!pickup.isActive) return; | |
const distance = this.player.position.distanceTo(pickup.position); | |
if (distance < 1.5) { // Pickup radius | |
// Apply pickup effect | |
switch (pickup.type) { | |
case 'health': | |
if (this.player.health < 100) { | |
this.player.health = Math.min(100, this.player.health + pickup.value); | |
this.updateHealthHUD(); | |
pickup.collect(); | |
sss.playSoundEffect("powerUp"); | |
} | |
break; | |
case 'megaHealth': | |
if (this.player.health < 200) { | |
this.player.health = Math.min(200, this.player.health + pickup.value); | |
this.updateHealthHUD(); | |
pickup.collect(); | |
sss.playSoundEffect("powerUp"); | |
this.showMessage("Mega Health!"); | |
} | |
break; | |
case 'armor': | |
if (this.player.armor < 100) { | |
this.player.armor = Math.min(100, this.player.armor + pickup.value); | |
this.updateArmorHUD(); | |
pickup.collect(); | |
sss.playSoundEffect("powerUp"); | |
} | |
break; | |
case 'heavyArmor': | |
if (this.player.armor < 200) { | |
this.player.armor = Math.min(200, this.player.armor + pickup.value); | |
this.updateArmorHUD(); | |
pickup.collect(); | |
sss.playSoundEffect("powerUp"); | |
this.showMessage("Heavy Armor!"); | |
} | |
break; | |
case 'ammo': | |
let weaponRefilled = false; | |
// Add ammo to all weapons | |
this.weapons.forEach(weapon => { | |
const maxAmmo = weapon.name === "MACHINE GUN" ? 200 : | |
weapon.name === "SHOTGUN" ? 50 : | |
weapon.name === "ROCKET LAUNCHER" ? 20 : | |
weapon.name === "RAILGUN" ? 20 : 100; | |
if (weapon.ammo < maxAmmo) { | |
weapon.ammo = Math.min(maxAmmo, weapon.ammo + pickup.value); | |
weaponRefilled = true; | |
} | |
}); | |
if (weaponRefilled) { | |
this.updateAmmoHUD(); | |
pickup.collect(); | |
sss.playSoundEffect("powerUp"); | |
} | |
break; | |
} | |
} | |
}); | |
} | |
updatePickups(deltaTime) { | |
this.pickups.forEach(pickup => pickup.update(deltaTime)); | |
} | |
respawnPlayer() { | |
// Reset player state | |
this.player.position.copy(this.level.getRandomSpawnPoint()); | |
this.player.velocity.set(0, 0, 0); | |
this.player.health = 100; | |
this.player.armor = 0; | |
// Update HUD | |
this.updateHealthHUD(); | |
this.updateArmorHUD(); | |
// Show respawn message | |
this.showMessage("Respawned!"); | |
// Play respawn sound | |
sss.playSoundEffect("powerUp"); | |
} | |
updateBots(deltaTime) { | |
this.bots.forEach(bot => { | |
// Skip dead bots waiting to respawn | |
if (bot.health <= 0) { | |
bot.respawnTimer -= deltaTime; | |
if (bot.respawnTimer <= 0) { | |
// Respawn bot | |
bot.respawn(this.level.getRandomSpawnPoint()); | |
} | |
return; | |
} | |
// Update bot AI | |
bot.update(deltaTime, this.player.position, this.bots); | |
// Check if bot should fire weapon | |
if (bot.shouldFire && bot.canFireWeapon()) { | |
bot.lastFireTime = this.elapsedTime; | |
this.fireProjectile(bot, bot.currentWeapon); | |
} | |
}); | |
// Update bot list | |
this.updateBotList(); | |
} | |
animate() { | |
requestAnimationFrame(this.animate.bind(this)); | |
if (gamePaused || !gameActive) return; | |
// Calculate delta time | |
this.deltaTime = this.clock.getDelta(); | |
this.elapsedTime += this.deltaTime; | |
// Update timer | |
this.remainingTime -= this.deltaTime; | |
if (this.remainingTime <= 0) { | |
this.remainingTime = 0; | |
this.endMatch(); | |
} | |
this.updateTimerHUD(); | |
// Update player movement and shooting | |
this.player.update(this.deltaTime, this.level); | |
if (this.player.isShooting && this.player.canFireWeapon(this.elapsedTime)) { | |
this.player.lastFireTime = this.elapsedTime; | |
this.fireProjectile(this.player, this.player.currentWeapon); | |
} | |
// Handle player falling off the map | |
if (this.player.position.y < -50) { | |
this.playerScore.deaths++; | |
this.updateScoreHUD(); | |
this.respawnPlayer(); | |
this.showDeathMessage(); | |
} | |
// Update projectiles | |
this.updateProjectiles(this.deltaTime); | |
// Update pickups | |
this.updatePickups(this.deltaTime); | |
// Check for player collisions with pickups | |
this.checkPickupCollisions(); | |
// Update bots | |
this.updateBots(this.deltaTime); | |
// Update minimap | |
this.updateMinimap(); | |
// Update respawn timer if player is dead | |
if (this.player.health <= 0) { | |
this.player.respawnTimer -= this.deltaTime; | |
if (this.player.respawnTimer <= 0) { | |
this.respawnPlayer(); | |
} | |
} | |
// Render scene | |
this.renderer.render(this.scene, this.camera); | |
} | |
endMatch() { | |
// Show end game message | |
this.showMessage(`Match Complete! Final Score: ${this.playerScore.kills} kills`, 5000); | |
// Return to home screen after delay | |
setTimeout(() => { | |
gameActive = false; | |
document.exitPointerLock(); | |
document.getElementById('homeScreen').style.display = 'flex'; | |
document.getElementById('playButton').style.display = 'block'; | |
document.getElementById('resumeButton').style.display = 'none'; | |
}, 5000); | |
} | |
} | |
// Player class | |
class Player { | |
constructor(camera, scene) { | |
this.camera = camera; | |
this.scene = scene; | |
this.position = new THREE.Vector3(0, 2, 0); | |
this.velocity = new THREE.Vector3(0, 0, 0); | |
this.moveSpeed = 10; | |
this.jumpForce = 15; | |
this.gravity = 30; | |
this.onGround = false; | |
this.height = 1.8; | |
this.radius = 0.5; | |
this.isMovingForward = false; | |
this.isMovingBackward = false; | |
this.isMovingLeft = false; | |
this.isMovingRight = false; | |
this.isJumping = false; | |
this.isShooting = false; | |
this.health = 100; | |
this.armor = 0; | |
this.currentWeapon = null; | |
this.lastFireTime = 0; | |
this.respawnTimer = 3; | |
// Create collision mesh for player | |
const geometry = new THREE.CylinderGeometry(this.radius, this.radius, this.height, 8); | |
const material = new THREE.MeshBasicMaterial({ color: 0x33ff33, wireframe: true, visible: false }); | |
this.mesh = new THREE.Mesh(geometry, material); | |
this.scene.add(this.mesh); | |
// Initialize camera position | |
this.camera.position.set(0, this.height, 0); | |
this.updateCameraPosition(); | |
} | |
reset() { | |
this.position = new THREE.Vector3(0, 2, 0); | |
this.velocity = new THREE.Vector3(0, 0, 0); | |
this.health = 100; | |
this.armor = 0; | |
this.respawnTimer = 3; | |
this.updateCameraPosition(); | |
} | |
handleKeyDown(e) { | |
switch (e.key.toLowerCase()) { | |
case 'w': this.isMovingForward = true; break; | |
case 's': this.isMovingBackward = true; break; | |
case 'a': this.isMovingLeft = true; break; | |
case 'd': this.isMovingRight = true; break; | |
case ' ': if (this.onGround) this.jump(); break; | |
} | |
} | |
handleKeyUp(e) { | |
switch (e.key.toLowerCase()) { | |
case 'w': this.isMovingForward = false; break; | |
case 's': this.isMovingBackward = false; break; | |
case 'a': this.isMovingLeft = false; break; | |
case 'd': this.isMovingRight = false; break; | |
} | |
} | |
handleMouseMove(e) { | |
const sensitivity = 0.002; | |
// Rotate camera around X axis (look up/down) | |
this.camera.rotation.x -= e.movementY * sensitivity; | |
// Clamp vertical rotation to prevent camera flipping | |
this.camera.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, this.camera.rotation.x)); | |
// Rotate camera around Y axis (look left/right) | |
this.camera.rotation.y -= e.movementX * sensitivity; | |
} | |
jump() { | |
if (this.onGround) { | |
this.velocity.y = this.jumpForce; | |
this.onGround = false; | |
sss.playSoundEffect("jump"); | |
} | |
} | |
canFireWeapon(currentTime) { | |
return currentTime - this.lastFireTime > this.currentWeapon.fireRate; | |
} | |
update(deltaTime, level) { | |
if (this.health <= 0) return; | |
// Calculate movement direction | |
const moveDirection = new THREE.Vector3(0, 0, 0); | |
// Get forward and right vectors from camera | |
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(this.camera.quaternion); | |
forward.y = 0; // Keep movement on xz plane | |
forward.normalize(); | |
const right = new THREE.Vector3(1, 0, 0).applyQuaternion(this.camera.quaternion); | |
right.y = 0; // Keep movement on xz plane | |
right.normalize(); | |
// Calculate movement based on input | |
if (this.isMovingForward) moveDirection.add(forward); | |
if (this.isMovingBackward) moveDirection.sub(forward); | |
if (this.isMovingRight) moveDirection.add(right); | |
if (this.isMovingLeft) moveDirection.sub(right); | |
// Normalize if moving in multiple directions | |
if (moveDirection.length() > 0) { | |
moveDirection.normalize(); | |
} | |
// Apply movement speed | |
moveDirection.multiplyScalar(this.moveSpeed * deltaTime); | |
// Apply gravity | |
this.velocity.y -= this.gravity * deltaTime; | |
// Update position | |
this.position.x += moveDirection.x; | |
this.position.z += moveDirection.z; | |
this.position.y += this.velocity.y * deltaTime; | |
// Check for collisions with level | |
const collisions = level.checkCollisions( | |
this.position, | |
this.radius, | |
this.height | |
); | |
// Resolve collisions | |
if (collisions.horizontal) { | |
this.position.x -= moveDirection.x; | |
this.position.z -= moveDirection.z; | |
} | |
if (collisions.vertical) { | |
this.velocity.y = 0; | |
if (collisions.below) { | |
this.onGround = true; | |
} | |
} | |
// Update mesh position | |
this.mesh.position.copy(this.position); | |
// Update camera position | |
this.updateCameraPosition(); | |
} | |
updateCameraPosition() { | |
// Position camera at player's eye level | |
this.camera.position.set( | |
this.position.x, | |
this.position.y + this.height * 0.8, // Slightly below top of player | |
this.position.z | |
); | |
} | |
takeDamage(amount, attacker) { | |
// Apply armor reduction if available | |
let damageToHealth = amount; | |
if (this.armor > 0) { | |
// Armor absorbs 2/3 of damage | |
const damageToArmor = Math.min(this.armor, amount * (2/3)); | |
this.armor -= damageToArmor; | |
damageToHealth = amount - damageToArmor; | |
} | |
this.health -= damageToHealth; | |
// Check for death | |
if (this.health <= 0) { | |
this.health = 0; | |
this.die(attacker); | |
} else { | |
// Play hurt sound | |
sss.playSoundEffect("hit"); | |
} | |
} | |
die(attacker) { | |
// Increment death count | |
window.game.playerScore.deaths++; | |
window.game.updateScoreHUD(); | |
// Show death message | |
window.game.showDeathMessage(); | |
// Set respawn timer | |
this.respawnTimer = 3; | |
// Play death sound | |
sss.playSoundEffect("explosion"); | |
} | |
} | |
// Bot class | |
class Bot { | |
constructor(scene, position, name, weapon) { | |
this.scene = scene; | |
this.position = position.clone(); | |
this.name = name; | |
this.direction = new THREE.Vector3(1, 0, 0); // Initial facing direction | |
this.velocity = new THREE.Vector3(); | |
this.moveSpeed = 7 + Math.random() * 3; // Slightly randomized speed | |
this.health = 100; | |
this.maxHealth = 100; | |
this.currentWeapon = weapon; | |
this.lastFireTime = 0; | |
this.fireRate = weapon.fireRate; | |
this.lastJumpTime = 0; | |
this.jumpInterval = 2 + Math.random() * 3; | |
this.shouldFire = false; | |
this.target = null; | |
this.respawnTimer = 3; | |
// Create bot mesh | |
this.createMesh(); | |
// Initialize waypoints for navigation | |
this.waypoints = []; | |
this.currentWaypoint = null; | |
this.waypointTimeout = 0; | |
// Random movement pattern | |
this.randomizeWaypoint(); | |
} | |
createMesh() { | |
// Create bot body | |
const bodyGeometry = new THREE.CylinderGeometry(0.5, 0.5, 1.8, 8); | |
const bodyMaterial = new THREE.MeshLambertMaterial({ color: 0xff3333 }); | |
this.mesh = new THREE.Mesh(bodyGeometry, bodyMaterial); | |
this.mesh.position.copy(this.position); | |
this.mesh.castShadow = true; | |
this.mesh.receiveShadow = true; | |
// Create bot head | |
const headGeometry = new THREE.BoxGeometry(0.6, 0.6, 0.6); | |
const headMaterial = new THREE.MeshLambertMaterial({ color: 0xffaa33 }); | |
this.head = new THREE.Mesh(headGeometry, headMaterial); | |
this.head.position.y = 1.2; | |
this.head.castShadow = true; | |
this.mesh.add(this.head); | |
// Create bot weapon | |
const weaponGeometry = new THREE.BoxGeometry(0.2, 0.2, 1); | |
const weaponMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 }); | |
this.weaponMesh = new THREE.Mesh(weaponGeometry, weaponMaterial); | |
this.weaponMesh.position.set(0.5, 0.3, 0.5); | |
this.mesh.add(this.weaponMesh); | |
this.scene.add(this.mesh); | |
} | |
update(deltaTime, playerPosition, bots) { | |
if (this.health <= 0) return; // Skip if dead | |
// Calculate distance to player | |
const distanceToPlayer = this.position.distanceTo(playerPosition); | |
// Decide if we should target the player | |
if (distanceToPlayer < 30 && Math.random() < 0.7) { | |
this.target = playerPosition; | |
this.waypointTimeout = 0; | |
// Check if we should shoot at player | |
if (distanceToPlayer < 20 && this.hasLineOfSight(playerPosition)) { | |
this.shouldFire = true; | |
// Look at player | |
this.direction.subVectors(playerPosition, this.position).normalize(); | |
this.head.lookAt(playerPosition); | |
} else { | |
this.shouldFire = false; | |
} | |
} else { | |
// Navigate by waypoints if not targeting player | |
this.shouldFire = false; | |
if (!this.currentWaypoint || this.waypointTimeout <= 0) { | |
this.randomizeWaypoint(); | |
} else { | |
this.target = this.currentWaypoint; | |
this.waypointTimeout -= deltaTime; | |
} | |
// Check if we reached the waypoint | |
if (this.currentWaypoint && this.position.distanceTo(this.currentWaypoint) < 2) { | |
this.randomizeWaypoint(); | |
} | |
// Check if we should target another bot | |
if (Math.random() < 0.02) { | |
const nearbyBots = bots.filter(bot => | |
bot !== this && | |
bot.health > 0 && | |
this.position.distanceTo(bot.position) < 15 | |
); | |
if (nearbyBots.length > 0) { | |
const targetBot = nearbyBots[Math.floor(Math.random() * nearbyBots.length)]; | |
this.target = targetBot.position; | |
if (this.hasLineOfSight(targetBot.position)) { | |
this.shouldFire = true; | |
this.direction.subVectors(targetBot.position, this.position).normalize(); | |
this.head.lookAt(targetBot.position); | |
} | |
} | |
} | |
} | |
// Move towards target | |
if (this.target) { | |
// Calculate direction to target | |
const directionToTarget = new THREE.Vector3() | |
.subVectors(this.target, this.position) | |
.normalize(); | |
// Apply movement | |
const moveAmount = this.moveSpeed * deltaTime; | |
this.position.x += directionToTarget.x * moveAmount; | |
this.position.z += directionToTarget.z * moveAmount; | |
// Update direction for weapon firing | |
if (!this.shouldFire) { | |
this.direction.copy(directionToTarget); | |
this.head.lookAt(this.target); | |
} | |
} | |
// Handle jumping (random jumps) | |
if (Math.random() < 0.01) { | |
this.jump(); | |
} | |
// Apply gravity | |
this.position.y += this.velocity.y * deltaTime; | |
this.velocity.y -= 30 * deltaTime; | |
// Prevent falling below ground | |
if (this.position.y < 1) { | |
this.position.y = 1; | |
this.velocity.y = 0; | |
} | |
// Update mesh position | |
this.mesh.position.copy(this.position); | |
// Make the bot face the direction it's moving | |
if (this.direction.length() > 0) { | |
this.mesh.lookAt( | |
this.position.x + this.direction.x, | |
this.position.y, | |
this.position.z + this.direction.z | |
); | |
} | |
} | |
randomizeWaypoint() { | |
// Pick a random point in the arena | |
this.currentWaypoint = new THREE.Vector3( | |
(Math.random() - 0.5) * 50, | |
1, | |
(Math.random() - 0.5) * 50 | |
); | |
// Set a timeout for this waypoint | |
this.waypointTimeout = 3 + Math.random() * 5; | |
} | |
jump() { | |
const currentTime = performance.now() / 1000; | |
if (currentTime - this.lastJumpTime > this.jumpInterval) { | |
this.velocity.y = 10; | |
this.lastJumpTime = currentTime; | |
} | |
} | |
hasLineOfSight(targetPosition) { | |
// Create a raycaster from bot to target | |
const raycaster = new THREE.Raycaster(); | |
const direction = new THREE.Vector3() | |
.subVectors(targetPosition, this.position) | |
.normalize(); | |
raycaster.set(this.position, direction); | |
// Get distance to target | |
const distanceToTarget = this.position.distanceTo(targetPosition); | |
// Check for obstacles | |
const intersects = raycaster.intersectObjects(window.game.scene.children, true); | |
for (const intersect of intersects) { | |
// Skip own mesh or other bots | |
if (intersect.object === this.mesh || | |
intersect.object.parent === this.mesh || | |
window.game.bots.some(bot => bot.mesh === intersect.object) || | |
intersect.object.isProjectile) { | |
continue; | |
} | |
// If we hit something before target, there's no line of sight | |
if (intersect.distance < distanceToTarget) { | |
return false; | |
} | |
} | |
return true; | |
} | |
canFireWeapon() { | |
return window.game.elapsedTime - this.lastFireTime > this.currentWeapon.fireRate; | |
} | |
takeDamage(amount, attacker) { | |
this.health -= amount; | |
// Check for death | |
if (this.health <= 0) { | |
this.health = 0; | |
this.die(attacker); | |
} | |
} | |
die(attacker) { | |
// Hide mesh | |
this.mesh.visible = false; | |
// Set respawn timer | |
this.respawnTimer = 3; | |
// Play death sound | |
sss.playSoundEffect("explosion"); | |
} | |
respawn(position) { | |
// Reset health and position | |
this.health = this.maxHealth; | |
this.position.copy(position); | |
this.mesh.position.copy(this.position); | |
// Make visible again | |
this.mesh.visible = true; | |
// Reset respawn timer | |
this.respawnTimer = 3; | |
// Randomize new weapon | |
this.currentWeapon = window.game.weapons[Math.floor(Math.random() * window.game.weapons.length)]; | |
// Randomize new waypoint | |
this.randomizeWaypoint(); | |
} | |
} | |
// Weapon class | |
class Weapon { | |
constructor(data, scene) { | |
this.scene = scene; | |
this.name = data.name; | |
this.damage = data.damage; | |
this.fireRate = data.fireRate; | |
this.ammo = data.ammo; | |
this.model = data.model; | |
this.projectileSpeed = data.projectileSpeed || 50; | |
this.projectileSize = data.projectileSize || 0.1; | |
this.projectileColor = data.projectileColor || 0xffff00; | |
this.isHitscan = data.isHitscan || false; | |
this.pellets = data.pellets || 1; | |
this.spread = data.spread || 0; | |
this.splashRadius = data.splashRadius || 0; | |
this.splashDamage = data.splashDamage || 0; | |
this.soundEffect = data.soundEffect || "laser"; | |
} | |
} | |
// Pickup class | |
class Pickup { | |
constructor(scene, type, model, position, respawnTime, value) { | |
this.scene = scene; | |
this.type = type; | |
this.model = model; | |
this.position = position.clone(); | |
this.respawnTime = respawnTime; | |
this.value = value; | |
this.isActive = true; | |
this.respawnTimer = 0; | |
this.createMesh(); | |
} | |
createMesh() { | |
// Create different meshes based on pickup type | |
let geometry, material; | |
switch (this.type) { | |
case 'health': | |
geometry = new THREE.BoxGeometry(0.8, 0.8, 0.8); | |
material = new THREE.MeshLambertMaterial({ color: 0xff3333 }); | |
break; | |
case 'megaHealth': | |
geometry = new THREE.BoxGeometry(1, 1, 1); | |
material = new THREE.MeshLambertMaterial({ color: 0xff00ff }); | |
break; | |
case 'armor': | |
geometry = new THREE.BoxGeometry(0.8, 0.8, 0.8); | |
material = new THREE.MeshLambertMaterial({ color: 0x3399ff }); | |
break; | |
case 'heavyArmor': | |
geometry = new THREE.BoxGeometry(1, 1, 1); | |
material = new THREE.MeshLambertMaterial({ color: 0x0000ff }); | |
break; | |
case 'ammo': | |
geometry = new THREE.BoxGeometry(0.8, 0.8, 0.8); | |
material = new THREE.MeshLambertMaterial({ color: 0xffcc00 }); | |
break; | |
default: | |
geometry = new THREE.SphereGeometry(0.5, 8, 8); | |
material = new THREE.MeshLambertMaterial({ color: 0xffffff }); | |
} | |
this.mesh = new THREE.Mesh(geometry, material); | |
this.mesh.position.copy(this.position); | |
this.mesh.castShadow = true; | |
this.mesh.receiveShadow = true; | |
// Create floating effect | |
this.floatHeight = this.position.y; | |
this.floatOffset = 0; | |
this.scene.add(this.mesh); | |
} | |
update(deltaTime) { | |
if (!this.isActive) { | |
// Update respawn timer | |
this.respawnTimer -= deltaTime; | |
if (this.respawnTimer <= 0) { | |
this.respawn(); | |
} | |
return; | |
} | |
// Floating animation | |
this.floatOffset += deltaTime * 2; | |
this.mesh.position.y = this.floatHeight + Math.sin(this.floatOffset) * 0.3; | |
// Rotation animation | |
this.mesh.rotation.y += deltaTime * 2; | |
} | |
collect() { | |
// Hide pickup | |
this.isActive = false; | |
this.mesh.visible = false; | |
// Start respawn timer | |
this.respawnTimer = this.respawnTime; | |
} | |
respawn() { | |
// Show pickup | |
this.isActive = true; | |
this.mesh.visible = true; | |
} | |
} | |
// Level class | |
class Level { | |
constructor(scene) { | |
this.scene = scene; | |
this.spawnPoints = []; | |
this.createArena(); | |
} | |
createArena() { | |
// Create floor | |
const floorGeometry = new THREE.BoxGeometry(100, 1, 100); | |
const floorMaterial = new THREE.MeshLambertMaterial({ | |
color: 0x666666, | |
map: this.createTexture('floor', 0x333333, 0x555555) | |
}); | |
const floor = new THREE.Mesh(floorGeometry, floorMaterial); | |
floor.position.set(0, -0.5, 0); | |
floor.receiveShadow = true; | |
this.scene.add(floor); | |
// Create spawn points | |
this.createSpawnPoints(); | |
// Create platforms | |
this.createPlatforms(); | |
// Create walls | |
this.createWalls(); | |
// Create pillars | |
this.createPillars(); | |
// Create ramps | |
this.createRamps(); | |
} | |
createTexture(type, color1, color2) { | |
const canvas = document.createElement('canvas'); | |
canvas.width = 256; | |
canvas.height = 256; | |
const context = canvas.getContext('2d'); | |
switch (type) { | |
case 'floor': | |
// Grid pattern | |
context.fillStyle = `#${color1.toString(16).padStart(6, '0')}`; | |
context.fillRect(0, 0, 256, 256); | |
context.fillStyle = `#${color2.toString(16).padStart(6, '0')}`; | |
for (let i = 0; i < 16; i++) { | |
context.fillRect(i * 16, 0, 1, 256); | |
context.fillRect(0, i * 16, 256, 1); | |
} | |
break; | |
case 'wall': | |
// Brick pattern | |
context.fillStyle = `#${color1.toString(16).padStart(6, '0')}`; | |
context.fillRect(0, 0, 256, 256); | |
context.fillStyle = `#${color2.toString(16).padStart(6, '0')}`; | |
for (let y = 0; y < 16; y++) { | |
const offset = (y % 2) * 16; | |
for (let x = 0; x < 8; x++) { | |
context.fillRect(offset + x * 32, y * 16, 30, 14); | |
} | |
} | |
break; | |
} | |
const texture = new THREE.CanvasTexture(canvas); | |
texture.wrapS = THREE.RepeatWrapping; | |
texture.wrapT = THREE.RepeatWrapping; | |
texture.repeat.set(4, 4); | |
return texture; | |
} | |
createSpawnPoints() { | |
// Add spawn points around the map | |
const points = [ | |
new THREE.Vector3(20, 2, 20), | |
new THREE.Vector3(-20, 2, 20), | |
new THREE.Vector3(20, 2, -20), | |
new THREE.Vector3(-20, 2, -20), | |
new THREE.Vector3(0, 2, 25), | |
new THREE.Vector3(0, 2, -25), | |
new THREE.Vector3(25, 2, 0), | |
new THREE.Vector3(-25, 2, 0), | |
new THREE.Vector3(15, 2, 15), | |
new THREE.Vector3(-15, 2, 15), | |
new THREE.Vector3(15, 2, -15), | |
new THREE.Vector3(-15, 2, -15), | |
new THREE.Vector3(0, 8, 0) // Central platform | |
]; | |
this.spawnPoints = points; | |
} | |
getRandomSpawnPoint() { | |
return this.spawnPoints[ | |
Math.floor(Math.random() * this.spawnPoints.length) | |
].clone(); | |
} | |
createPlatforms() { | |
// Central platform | |
const centralPlatformGeometry = new THREE.BoxGeometry(10, 1, 10); | |
const centralPlatformMaterial = new THREE.MeshLambertMaterial({ | |
color: 0x888888, | |
map: this.createTexture('floor', 0x777777, 0x999999) | |
}); | |
const centralPlatform = new THREE.Mesh(centralPlatformGeometry, centralPlatformMaterial); | |
centralPlatform.position.set(0, 7, 0); | |
centralPlatform.receiveShadow = true; | |
centralPlatform.castShadow = true; | |
this.scene.add(centralPlatform); | |
// Platform base pillars | |
const pillarGeometry = new THREE.BoxGeometry(2, 14, 2); | |
const pillarMaterial = new THREE.MeshLambertMaterial({ color: 0x444444 }); | |
const pillar1 = new THREE.Mesh(pillarGeometry, pillarMaterial); | |
pillar1.position.set(4, 0, 4); | |
pillar1.receiveShadow = true; | |
pillar1.castShadow = true; | |
this.scene.add(pillar1); | |
const pillar2 = new THREE.Mesh(pillarGeometry, pillarMaterial); | |
pillar2.position.set(-4, 0, 4); | |
pillar2.receiveShadow = true; | |
pillar2.castShadow = true; | |
this.scene.add(pillar2); | |
const pillar3 = new THREE.Mesh(pillarGeometry, pillarMaterial); | |
pillar3.position.set(4, 0, -4); | |
pillar3.receiveShadow = true; | |
pillar3.castShadow = true; | |
this.scene.add(pillar3); | |
const pillar4 = new THREE.Mesh(pillarGeometry, pillarMaterial); | |
pillar4.position.set(-4, 0, -4); | |
pillar4.receiveShadow = true; | |
pillar4.castShadow = true; | |
this.scene.add(pillar4); | |
// Corner platforms | |
const cornerPlatformGeometry = new THREE.BoxGeometry(8, 1, 8); | |
const cornerPlatformMaterial = new THREE.MeshLambertMaterial({ | |
color: 0x888888, | |
map: this.createTexture('floor', 0x777777, 0x999999) | |
}); | |
const cornerPositions = [ | |
{ x: 20, y: 4, z: 20 }, | |
{ x: -20, y: 4, z: 20 }, | |
{ x: 20, y: 4, z: -20 }, | |
{ x: -20, y: 4, z: -20 } | |
]; | |
cornerPositions.forEach(pos => { | |
const platform = new THREE.Mesh(cornerPlatformGeometry, cornerPlatformMaterial); | |
platform.position.set(pos.x, pos.y, pos.z); | |
platform.receiveShadow = true; | |
platform.castShadow = true; | |
this.scene.add(platform); | |
// Small pillar for the platform | |
const smallPillarGeometry = new THREE.BoxGeometry(4, 8, 4); | |
const smallPillar = new THREE.Mesh(smallPillarGeometry, pillarMaterial); | |
smallPillar.position.set(pos.x, pos.y - 4, pos.z); | |
smallPillar.receiveShadow = true; | |
smallPillar.castShadow = true; | |
this.scene.add(smallPillar); | |
}); | |
// Side platforms | |
const sidePlatformGeometry = new THREE.BoxGeometry(6, 1, 6); | |
const sidePlatformMaterial = new THREE.MeshLambertMaterial({ | |
color: 0x888888, | |
map: this.createTexture('floor', 0x777777, 0x999999) | |
}); | |
const sidePositions = [ | |
{ x: 0, y: 3, z: 25 }, | |
{ x: 0, y: 3, z: -25 }, | |
{ x: 25, y: 3, z: 0 }, | |
{ x: -25, y: 3, z: 0 } | |
]; | |
sidePositions.forEach(pos => { | |
const platform = new THREE.Mesh(sidePlatformGeometry, sidePlatformMaterial); | |
platform.position.set(pos.x, pos.y, pos.z); | |
platform.receiveShadow = true; | |
platform.castShadow = true; | |
this.scene.add(platform); | |
// Small pillar for the platform | |
const smallPillarGeometry = new THREE.BoxGeometry(3, 6, 3); | |
const smallPillar = new THREE.Mesh(smallPillarGeometry, pillarMaterial); | |
smallPillar.position.set(pos.x, pos.y - 3, pos.z); | |
smallPillar.receiveShadow = true; | |
smallPillar.castShadow = true; | |
this.scene.add(smallPillar); | |
}); | |
} | |
createWalls() { | |
// Outer walls | |
const wallHeight = 10; | |
const wallThickness = 1; | |
const wallMaterial = new THREE.MeshLambertMaterial({ | |
color: 0x555555, | |
map: this.createTexture('wall', 0x444444, 0x666666) | |
}); | |
// North wall | |
const northWallGeometry = new THREE.BoxGeometry(100, wallHeight, wallThickness); | |
const northWall = new THREE.Mesh(northWallGeometry, wallMaterial); | |
northWall.position.set(0, wallHeight / 2 - 0.5, -50); | |
northWall.receiveShadow = true; | |
northWall.castShadow = true; | |
this.scene.add(northWall); | |
// South wall | |
const southWallGeometry = new THREE.BoxGeometry(100, wallHeight, wallThickness); | |
const southWall = new THREE.Mesh(southWallGeometry, wallMaterial); | |
southWall.position.set(0, wallHeight / 2 - 0.5, 50); | |
southWall.receiveShadow = true; | |
southWall.castShadow = true; | |
this.scene.add(southWall); | |
// East wall | |
const eastWallGeometry = new THREE.BoxGeometry(wallThickness, wallHeight, 100); | |
const eastWall = new THREE.Mesh(eastWallGeometry, wallMaterial); | |
eastWall.position.set(50, wallHeight / 2 - 0.5, 0); | |
eastWall.receiveShadow = true; | |
eastWall.castShadow = true; | |
this.scene.add(eastWall); | |
// West wall | |
const westWallGeometry = new THREE.BoxGeometry(wallThickness, wallHeight, 100); | |
const westWall = new THREE.Mesh(westWallGeometry, wallMaterial); | |
westWall.position.set(-50, wallHeight / 2 - 0.5, 0); | |
westWall.receiveShadow = true; | |
westWall.castShadow = true; | |
this.scene.add(westWall); | |
} | |
createPillars() { | |
// Create decorative pillars | |
const pillarGeometry = new THREE.CylinderGeometry(1, 1, 12, 8); | |
const pillarMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 }); | |
const pillarPositions = [ | |
{ x: 15, z: 15 }, | |
{ x: -15, z: 15 }, | |
{ x: 15, z: -15 }, | |
{ x: -15, z: -15 }, | |
{ x: 35, z: 35 }, | |
{ x: -35, z: 35 }, | |
{ x: 35, z: -35 }, | |
{ x: -35, z: -35 } | |
]; | |
pillarPositions.forEach(pos => { | |
const pillar = new THREE.Mesh(pillarGeometry, pillarMaterial); | |
pillar.position.set(pos.x, 6 - 0.5, pos.z); | |
pillar.receiveShadow = true; | |
pillar.castShadow = true; | |
this.scene.add(pillar); | |
}); | |
} | |
createRamps() { | |
// Create ramps to access elevated platforms | |
const rampMaterial = new THREE.MeshLambertMaterial({ | |
color: 0x777777, | |
map: this.createTexture('floor', 0x666666, 0x888888) | |
}); | |
// Helper function to create a ramp | |
const createRamp = (x, z, rotation) => { | |
// Create ramp as a box geometry | |
const rampGeometry = new THREE.BoxGeometry(8, 1, 12); | |
const ramp = new THREE.Mesh(rampGeometry, rampMaterial); | |
// Position and rotate | |
ramp.position.set(x, 1.5, z); | |
ramp.rotation.x = rotation * Math.PI / 180; | |
// Rotate around Y if needed | |
if (x !== 0) { | |
ramp.rotation.y = 90 * Math.PI / 180; | |
} | |
ramp.receiveShadow = true; | |
ramp.castShadow = true; | |
this.scene.add(ramp); | |
}; | |
// Create ramps for the side platforms | |
createRamp(0, 19, 15); // North | |
createRamp(0, -19, -15); // South | |
createRamp(19, 0, 15); // East | |
createRamp(-19, 0, -15); // West | |
// Create ramps for corner platforms | |
createRamp(15, 15, 20); // Northeast | |
createRamp(-15, 15, -20); // Northwest | |
createRamp(15, -15, -20); // Southeast | |
createRamp(-15, -15, 20); // Southwest | |
// Create spiral ramp to central platform | |
this.createSpiralRamp(); | |
} | |
createSpiralRamp() { | |
const rampMaterial = new THREE.MeshLambertMaterial({ | |
color: 0x777777, | |
map: this.createTexture('floor', 0x666666, 0x888888) | |
}); | |
// Create spiral segments | |
const segments = 12; | |
const heightStep = 0.5; | |
const radius = 8; | |
const width = 3; | |
for (let i = 0; i < segments; i++) { | |
const angle = (i / segments) * Math.PI * 2; | |
const nextAngle = ((i + 1) / segments) * Math.PI * 2; | |
const x1 = Math.cos(angle) * radius; | |
const z1 = Math.sin(angle) * radius; | |
const y1 = 0.5 + i * heightStep; | |
const x2 = Math.cos(nextAngle) * radius; | |
const z2 = Math.sin(nextAngle) * radius; | |
const y2 = 0.5 + (i + 1) * heightStep; | |
// Create segment | |
const shape = new THREE.Shape(); | |
shape.moveTo(0, 0); | |
shape.lineTo(width, 0); | |
shape.lineTo(width, Math.sqrt((x2-x1)**2 + (z2-z1)**2)); | |
shape.lineTo(0, Math.sqrt((x2-x1)**2 + (z2-z1)**2)); | |
const extrudeSettings = { | |
steps: 1, | |
depth: 1, | |
bevelEnabled: false | |
}; | |
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings); | |
const segment = new THREE.Mesh(geometry, rampMaterial); | |
// Position and rotate the segment | |
segment.position.set(x1, y1, z1); | |
segment.lookAt(x2, y2, z2); | |
segment.rotation.x -= Math.PI / 2; | |
segment.receiveShadow = true; | |
segment.castShadow = true; | |
this.scene.add(segment); | |
} | |
} | |
checkCollisions(position, radius, height) { | |
const result = { | |
horizontal: false, | |
vertical: false, | |
below: false | |
}; | |
// Check floor collision | |
if (position.y < 0) { | |
position.y = 0; | |
result.vertical = true; | |
result.below = true; | |
} | |
// Check collision with central platform | |
if (Math.abs(position.x) < 5 && Math.abs(position.z) < 5 && position.y < 7.5 && position.y > 5.5) { | |
position.y = 7.5; | |
result.vertical = true; | |
result.below = true; | |
} | |
// Check collision with corner platforms | |
const cornerPlatforms = [ | |
{ x: 20, y: 4, z: 20, width: 8, depth: 8 }, | |
{ x: -20, y: 4, z: 20, width: 8, depth: 8 }, | |
{ x: 20, y: 4, z: -20, width: 8, depth: 8 }, | |
{ x: -20, y: 4, z: -20, width: 8, depth: 8 } | |
]; | |
cornerPlatforms.forEach(platform => { | |
if (Math.abs(position.x - platform.x) < platform.width / 2 + radius && | |
Math.abs(position.z - platform.z) < platform.depth / 2 + radius) { | |
// Check if we're on top of the platform | |
if (position.y < platform.y + 0.5 && position.y > platform.y - 1) { | |
position.y = platform.y + 0.5; | |
result.vertical = true; | |
result.below = true; | |
} | |
// Check if we're hitting the platform from the side | |
else if (position.y < platform.y && position.y + height > platform.y - 4) { | |
result.horizontal = true; | |
} | |
} | |
}); | |
// Check collision with side platforms | |
const sidePlatforms = [ | |
{ x: 0, y: 3, z: 25, width: 6, depth: 6 }, | |
{ x: 0, y: 3, z: -25, width: 6, depth: 6 }, | |
{ x: 25, y: 3, z: 0, width: 6, depth: 6 }, | |
{ x: -25, y: 3, z: 0, width: 6, depth: 6 } | |
]; | |
sidePlatforms.forEach(platform => { | |
if (Math.abs(position.x - platform.x) < platform.width / 2 + radius && | |
Math.abs(position.z - platform.z) < platform.depth / 2 + radius) { | |
// Check if we're on top of the platform | |
if (position.y < platform.y + 0.5 && position.y > platform.y - 1) { | |
position.y = platform.y + 0.5; | |
result.vertical = true; | |
result.below = true; | |
} | |
// Check if we're hitting the platform from the side | |
else if (position.y < platform.y && position.y + height > platform.y - 3) { | |
result.horizontal = true; | |
} | |
} | |
}); | |
// Check collision with outer walls | |
if (Math.abs(position.x) > 48.5 - radius) { | |
position.x = Math.sign(position.x) * (48.5 - radius); | |
result.horizontal = true; | |
} | |
if (Math.abs(position.z) > 48.5 - radius) { | |
position.z = Math.sign(position.z) * (48.5 - radius); | |
result.horizontal = true; | |
} | |
// Check collision with central pillars | |
const pillars = [ | |
{ x: 4, z: 4, radius: 1.5 }, | |
{ x: -4, z: 4, radius: 1.5 }, | |
{ x: 4, z: -4, radius: 1.5 }, | |
{ x: -4, z: -4, radius: 1.5 } | |
]; | |
pillars.forEach(pillar => { | |
const dx = position.x - pillar.x; | |
const dz = position.z - pillar.z; | |
const distance = Math.sqrt(dx * dx + dz * dz); | |
if (distance < pillar.radius + radius) { | |
// Push player away from pillar | |
const angle = Math.atan2(dz, dx); | |
position.x = pillar.x + (pillar.radius + radius) * Math.cos(angle); | |
position.z = pillar.z + (pillar.radius + radius) * Math.sin(angle); | |
result.horizontal = true; | |
} | |
}); | |
// Check collision with decorative pillars | |
const decorativePillars = [ | |
{ x: 15, z: 15 }, | |
{ x: -15, z: 15 }, | |
{ x: 15, z: -15 }, | |
{ x: -15, z: -15 }, | |
{ x: 35, z: 35 }, | |
{ x: -35, z: 35 }, | |
{ x: 35, z: -35 }, | |
{ x: -35, z: -35 } | |
]; | |
decorativePillars.forEach(pillar => { | |
const dx = position.x - pillar.x; | |
const dz = position.z - pillar.z; | |
const distance = Math.sqrt(dx * dx + dz * dz); | |
if (distance < 1.5 + radius) { | |
// Push player away from pillar | |
const angle = Math.atan2(dz, dx); | |
position.x = pillar.x + (1.5 + radius) * Math.cos(angle); | |
position.z = pillar.z + (1.5 + radius) * Math.sin(angle); | |
result.horizontal = true; | |
} | |
}); | |
return result; | |
} | |
} | |
// Initialize game on window load | |
window.addEventListener('load', () => { | |
window.game = new QuakeArenaGame(); | |
// Simulate loading progress completion | |
const loadingInterval = setInterval(() => { | |
const progress = document.getElementById('loadingProgress'); | |
const currentWidth = parseFloat(progress.style.width || '0'); | |
if (currentWidth >= 100) { | |
clearInterval(loadingInterval); | |
setTimeout(() => { | |
document.getElementById('loadingScreen').style.display = 'none'; | |
}, 500); | |
} else { | |
progress.style.width = `${Math.min(currentWidth + 5, 100)}%`; | |
} | |
}, 100); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment