Skip to content

Instantly share code, notes, and snippets.

@ashleyrudland
Created March 2, 2025 00:52
Show Gist options
  • Save ashleyrudland/6b43cb4816347b1283e8dfc166a52242 to your computer and use it in GitHub Desktop.
Save ashleyrudland/6b43cb4816347b1283e8dfc166a52242 to your computer and use it in GitHub Desktop.
<!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