<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CYBERSTRIKE 3D FPS</title>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Three.js CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #050508;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
user-select: none;
-webkit-user-select: none;
}
#crosshair {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 10;
}
.hud-glass {
background: rgba(15, 23, 42, 0.65);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.glow-cyan {
box-shadow: 0 0 15px rgba(6, 182, 212, 0.4);
}
.glow-red {
box-shadow: 0 0 15px rgba(239, 68, 68, 0.4);
}
#damage-overlay {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none;
background: radial-gradient(circle, transparent 60%, rgba(239, 68, 68, 0.5) 100%);
opacity: 0;
transition: opacity 0.15s ease-out;
z-index: 5;
}
</style>
</head>
<body class="w-screen h-screen overflow-hidden text-white">
<!-- Damage Screen Flash -->
<div id="damage-overlay"></div>
<!-- Crosshair -->
<div id="crosshair" class="hidden">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" class="opacity-80">
<circle cx="12" cy="12" r="3" fill="#00f0ff"/>
<line x1="12" y1="2" x2="12" y2="7" stroke="#00f0ff" stroke-width="1.5" stroke-linecap="round"/>
<line x1="12" y1="17" x2="12" y2="22" stroke="#00f0ff" stroke-width="1.5" stroke-linecap="round"/>
<line x1="2" y1="12" x2="7" y2="12" stroke="#00f0ff" stroke-width="1.5" stroke-linecap="round"/>
<line x1="17" y1="12" x2="22" y2="12" stroke="#00f0ff" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</div>
<!-- HUD Overlay -->
<div id="hud" class="absolute inset-0 pointer-events-none hidden flex-col justify-between p-6 z-10">
<!-- Top Bar: Score & Wave -->
<div class="flex justify-between items-center w-full max-w-4xl mx-auto">
<div class="hud-glass px-5 py-2.5 rounded-2xl flex items-center gap-4 glow-cyan">
<div>
<div class="text-xs text-cyan-400 font-semibold uppercase tracking-wider">Score</div>
<div id="hud-score" class="text-2xl font-black tracking-tight text-white">0</div>
</div>
</div>
<div class="hud-glass px-6 py-2.5 rounded-2xl text-center glow-cyan">
<div class="text-xs text-cyan-400 font-semibold uppercase tracking-wider">Wave</div>
<div id="hud-wave" class="text-2xl font-black text-white">1</div>
</div>
<div class="hud-glass px-5 py-2.5 rounded-2xl flex items-center gap-4 glow-cyan">
<div class="text-right">
<div class="text-xs text-cyan-400 font-semibold uppercase tracking-wider">Enemies Left</div>
<div id="hud-enemies" class="text-2xl font-black text-white">0</div>
</div>
</div>
</div>
<!-- Bottom Bar: Health & Ammo -->
<div class="flex justify-between items-end w-full max-w-4xl mx-auto">
<!-- Health Bar -->
<div class="hud-glass p-4 rounded-2xl w-64 glow-red">
<div class="flex justify-between mb-1.5 items-center">
<span class="text-xs text-red-400 font-bold uppercase tracking-wider">Health</span>
<span id="hud-health-num" class="text-sm font-bold text-red-200">100 HP</span>
</div>
<div class="w-full h-3 bg-gray-900/80 rounded-full overflow-hidden p-0.5 border border-red-500/30">
<div id="hud-health-bar" class="h-full bg-gradient-to-r from-red-600 to-rose-400 rounded-full transition-all duration-200" style="width: 100%;"></div>
</div>
</div>
<!-- Reloading Notification Indicator -->
<div id="reload-msg" class="text-amber-400 font-black tracking-widest uppercase text-xl animate-pulse hidden mb-4">
[ Reloading... ]
</div>
<!-- Ammo Indicator -->
<div class="hud-glass p-4 rounded-2xl text-right w-44 glow-cyan">
<div class="text-xs text-cyan-400 font-bold uppercase tracking-wider mb-0.5">Ammo</div>
<div class="flex items-baseline justify-end gap-1">
<span id="hud-ammo" class="text-3xl font-black text-cyan-300">30</span>
<span class="text-gray-400 text-sm font-bold">/ <span id="hud-ammo-max">30</span></span>
</div>
</div>
</div>
</div>
<!-- Start / Title Screen Overlay -->
<div id="start-screen" class="absolute inset-0 bg-slate-950/90 backdrop-blur-md flex flex-col items-center justify-center z-20">
<div class="max-w-md w-full p-8 rounded-3xl hud-glass text-center shadow-2xl border border-cyan-500/30">
<h1 class="text-4xl md:text-5xl font-black tracking-wider text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 via-sky-300 to-indigo-400 mb-2">
CYBERSTRIKE 3D
</h1>
<p class="text-slate-400 text-sm mb-8">Defend the arena against cybernetic rogue drones.</p>
<div class="bg-slate-900/60 p-4 rounded-xl text-left text-xs text-slate-300 space-y-2 mb-8 border border-white/5">
<div class="flex justify-between"><span class="text-cyan-400 font-bold">WASD / Arrows</span> Move</div>
<div class="flex justify-between"><span class="text-cyan-400 font-bold">Mouse Move</span> Look Around</div>
<div class="flex justify-between"><span class="text-cyan-400 font-bold">Left Click</span> Shoot</div>
<div class="flex justify-between"><span class="text-cyan-400 font-bold">R</span> Reload Weapon</div>
<div class="flex justify-between"><span class="text-cyan-400 font-bold">Space</span> Jump</div>
</div>
<button id="btn-start" class="w-full py-4 bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-white font-bold rounded-xl tracking-wider uppercase transition transform hover:scale-[1.02] active:scale-[0.98] shadow-lg shadow-cyan-500/30">
Deploy to Arena
</button>
</div>
</div>
<!-- Pause / Lock Screen Overlay -->
<div id="pause-screen" class="absolute inset-0 bg-slate-950/80 backdrop-blur-md flex flex-col items-center justify-center z-20 hidden">
<div class="max-w-sm w-full p-8 rounded-3xl hud-glass text-center shadow-2xl border border-white/10">
<h2 class="text-3xl font-bold text-cyan-400 mb-4">GAME PAUSED</h2>
<p class="text-slate-300 text-sm mb-6">Click anywhere to resume target engagement.</p>
<button id="btn-resume" class="w-full py-3 bg-cyan-500 hover:bg-cyan-400 text-slate-950 font-bold rounded-xl tracking-wider uppercase transition">
Resume Game
</button>
</div>
</div>
<!-- Game Over Overlay -->
<div id="gameover-screen" class="absolute inset-0 bg-slate-950/95 backdrop-blur-md flex flex-col items-center justify-center z-20 hidden">
<div class="max-w-md w-full p-8 rounded-3xl hud-glass text-center shadow-2xl border border-red-500/30">
<h2 class="text-4xl font-black text-red-500 tracking-wider mb-2">SYSTEM FAILURE</h2>
<p class="text-slate-400 text-sm mb-6">Your chassis was overwhelmed by hostile forces.</p>
<div class="bg-slate-900/60 p-4 rounded-xl text-center space-y-2 mb-8 border border-white/5">
<div class="text-xs text-slate-400 uppercase font-bold">Final Score</div>
<div id="final-score" class="text-4xl font-black text-cyan-400">0</div>
<div id="final-wave" class="text-xs text-slate-400 font-medium">Survived until Wave 1</div>
</div>
<button id="btn-restart" class="w-full py-4 bg-gradient-to-r from-red-600 to-rose-500 hover:from-red-500 hover:to-rose-400 text-white font-bold rounded-xl tracking-wider uppercase transition transform hover:scale-[1.02] active:scale-[0.98] shadow-lg shadow-red-500/30">
Re-initialize Core
</button>
</div>
</div>
<!-- Three.js Canvas Container -->
<div id="game-container" class="w-full h-full"></div>
<script>
/**
* CYBERSTRIKE 3D FPS - Sound Synthesizer (Web Audio API)
* Generates sound effects without external assets.
*/
class SoundFX {
constructor() {
this.ctx = null;
}
init() {
if (!this.ctx) {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
}
}
playGunshot() {
if (!this.ctx) return;
const now = this.ctx.currentTime;
// Noise buffer for blast
const bufferSize = this.ctx.sampleRate * 0.15;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const output = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
output[i] = Math.random() * 2 - 1;
}
const whiteNoise = this.ctx.createBufferSource();
whiteNoise.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(1200, now);
filter.frequency.exponentialRampToValueAtTime(80, now + 0.15);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.4, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
whiteNoise.connect(filter);
filter.connect(gain);
gain.connect(this.ctx.destination);
whiteNoise.start(now);
// Transient punch oscillator
const osc = this.ctx.createOscillator();
const oscGain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(150, now);
osc.frequency.exponentialRampToValueAtTime(30, now + 0.1);
oscGain.gain.setValueAtTime(0.5, now);
oscGain.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
osc.connect(oscGain);
oscGain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + 0.1);
}
playHit() {
if (!this.ctx) return;
const now = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(800, now);
osc.frequency.exponentialRampToValueAtTime(400, now + 0.05);
gain.gain.setValueAtTime(0.2, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.05);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + 0.05);
}
playExplosion() {
if (!this.ctx) return;
const now = this.ctx.currentTime;
const bufferSize = this.ctx.sampleRate * 0.4;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const output = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
output[i] = Math.random() * 2 - 1;
}
const noise = this.ctx.createBufferSource();
noise.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(400, now);
filter.frequency.linearRampToValueAtTime(30, now + 0.4);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.6, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.4);
noise.connect(filter);
filter.connect(gain);
gain.connect(this.ctx.destination);
noise.start(now);
}
playPlayerHit() {
if (!this.ctx) return;
const now = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(120, now);
osc.frequency.exponentialRampToValueAtTime(40, now + 0.2);
gain.gain.setValueAtTime(0.4, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.2);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + 0.2);
}
playReload() {
if (!this.ctx) return;
const now = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(300, now);
osc.frequency.linearRampToValueAtTime(600, now + 0.15);
gain.gain.setValueAtTime(0.15, now);
gain.gain.linearRampToValueAtTime(0.01, now + 0.15);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + 0.15);
}
}
const sfx = new SoundFX();
/**
* GAME ENGINE & LOGIC
*/
const Game = {
// State Flags
started: false,
paused: false,
gameOver: false,
// Stats
score: 0,
wave: 1,
health: 100,
maxHealth: 100,
ammo: 30,
maxAmmo: 30,
isReloading: false,
// Spatial Constants
ARENA_SIZE: 100,
PLAYER_RADIUS: 1.2,
// Three.js Core Setup
scene: null,
camera: null,
renderer: null,
raycaster: null,
// Game Objects & Physics
obstacles: [],
enemies: [],
particles: [],
bullets: [],
// Player State
player: {
position: new THREE.Vector3(0, 1.7, 0),
velocity: new THREE.Vector3(),
pitch: 0,
yaw: 0,
onGround: true,
speed: 12,
jumpForce: 10
},
keys: {},
// Weapon Model & Rig
gunGroup: null,
recoilAmount: 0,
muzzleFlashLight: null,
// Clock & Loop Controls
clock: new THREE.Clock(),
// Initialize Game engine components
init() {
const container = document.getElementById('game-container');
// Scene Setup
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x05070f);
this.scene.fog = new THREE.FogExp2(0x05070f, 0.015);
// Camera Setup
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
this.camera.rotation.order = 'YXZ';
// Renderer Setup
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
container.appendChild(this.renderer.domElement);
// Raycaster for shooting logic
this.raycaster = new THREE.Raycaster();
// Build World Environment
this.setupLighting();
this.buildArena();
this.createGunModel();
// Setup Input Event Listeners
this.setupEvents();
// Window Resize Handler
window.addEventListener('resize', () => this.onWindowResize());
},
setupLighting() {
// Ambient Light
const ambientLight = new THREE.AmbientLight(0x1a2638, 1.2);
this.scene.add(ambientLight);
// Directional Sun Light with Shadows
const dirLight = new THREE.DirectionalLight(0x38bdf8, 1.5);
dirLight.position.set(30, 50, 20);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 150;
const d = 60;
dirLight.shadow.camera.left = -d;
dirLight.shadow.camera.right = d;
dirLight.shadow.camera.top = d;
dirLight.shadow.camera.bottom = -d;
this.scene.add(dirLight);
// Accent Point Lights in corners
const colors = [0x00f0ff, 0xff0055, 0x00ff88, 0x7928ca];
const corners = [
[40, 40], [-40, 40], [-40, -40], [40, -40]
];
corners.forEach((pos, idx) => {
const ptLight = new THREE.PointLight(colors[idx], 2, 35);
ptLight.position.set(pos[0], 6, pos[1]);
this.scene.add(ptLight);
// Light source pillar visual
const pillarGeo = new THREE.CylinderGeometry(0.2, 0.2, 12, 16);
const pillarMat = new THREE.MeshStandardMaterial({
color: colors[idx],
emissive: colors[idx],
emissiveIntensity: 0.8
});
const pillar = new THREE.Mesh(pillarGeo, pillarMat);
pillar.position.set(pos[0], 6, pos[1]);
this.scene.add(pillar);
});
},
buildArena() {
// Grid Floor Texture (Procedural Canvas)
const canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 512;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#090d16';
ctx.fillRect(0, 0, 512, 512);
ctx.strokeStyle = '#1e293b';
ctx.lineWidth = 4;
ctx.strokeRect(0, 0, 512, 512);
ctx.strokeStyle = '#0f172a';
ctx.lineWidth = 2;
for (let i = 0; i < 512; i += 32) {
ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, 512); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(512, i); ctx.stroke();
}
const floorTexture = new THREE.CanvasTexture(canvas);
floorTexture.wrapS = THREE.RepeatWrapping;
floorTexture.wrapT = THREE.RepeatWrapping;
floorTexture.repeat.set(25, 25);
// Floor Mesh
const floorGeo = new THREE.PlaneGeometry(this.ARENA_SIZE, this.ARENA_SIZE);
const floorMat = new THREE.MeshStandardMaterial({
map: floorTexture,
roughness: 0.4,
metalness: 0.2
});
const floor = new THREE.Mesh(floorGeo, floorMat);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
this.scene.add(floor);
// Perimeter Outer Walls
const wallMat = new THREE.MeshStandardMaterial({
color: 0x0f172a,
roughness: 0.6,
metalness: 0.5
});
const wallHeight = 12;
const wallGeoHorizontal = new THREE.BoxGeometry(this.ARENA_SIZE, wallHeight, 2);
const wallGeoVertical = new THREE.BoxGeometry(2, wallHeight, this.ARENA_SIZE);
// Wall 1 (North)
const w1 = new THREE.Mesh(wallGeoHorizontal, wallMat);
w1.position.set(0, wallHeight / 2, -this.ARENA_SIZE / 2);
// Wall 2 (South)
const w2 = new THREE.Mesh(wallGeoHorizontal, wallMat);
w2.position.set(0, wallHeight / 2, this.ARENA_SIZE / 2);
// Wall 3 (West)
const w3 = new THREE.Mesh(wallGeoVertical, wallMat);
w3.position.set(-this.ARENA_SIZE / 2, wallHeight / 2, 0);
// Wall 4 (East)
const w4 = new THREE.Mesh(wallGeoVertical, wallMat);
w4.position.set(this.ARENA_SIZE / 2, wallHeight / 2, 0);
[w1, w2, w3, w4].forEach(w => {
w.receiveShadow = true;
w.castShadow = true;
this.scene.add(w);
this.obstacles.push(new THREE.Box3().setFromObject(w));
});
// Spawn Internal Obstacles / Cover Blocks
const boxGeo = new THREE.BoxGeometry(4, 4, 4);
const boxMat = new THREE.MeshStandardMaterial({
color: 0x1e293b,
roughness: 0.3,
metalness: 0.8
});
// Structured placement layout for obstacles
const layoutPositions = [
{x: -15, z: -15}, {x: 15, z: -15}, {x: -15, z: 15}, {x: 15, z: 15},
{x: 0, z: -25}, {x: 0, z: 25}, {x: -25, z: 0}, {x: 25, z: 0},
{x: -30, z: -30}, {x: 30, z: -30}, {x: -30, z: 30}, {x: 30, z: 30}
];
layoutPositions.forEach(pos => {
const block = new THREE.Mesh(boxGeo, boxMat);
// Add random height stacking for dynamic cover
const scaleY = 1 + Math.floor(Math.random() * 2);
block.scale.set(1, scaleY, 1);
block.position.set(pos.x, (4 * scaleY) / 2, pos.z);
block.castShadow = true;
block.receiveShadow = true;
this.scene.add(block);
// Add cyan accent stripe on blocks
const stripeGeo = new THREE.BoxGeometry(4.1, 0.2, 4.1);
const stripeMat = new THREE.MeshBasicMaterial({ color: 0x00f0ff });
const stripe = new THREE.Mesh(stripeGeo, stripeMat);
stripe.position.set(0, 0, 0);
block.add(stripe);
// Store bounding box for physics collision
this.obstacles.push(new THREE.Box3().setFromObject(block));
});
},
createGunModel() {
// HUD Camera Setup (Separate rendering view layer for FPS weapon)
this.gunGroup = new THREE.Group();
// Futuristic Blaster Geometry Assembly
const bodyMat = new THREE.MeshStandardMaterial({ color: 0x111827, metalness: 0.9, roughness: 0.2 });
const accentMat = new THREE.MeshStandardMaterial({ color: 0x00f0ff, emissive: 0x00f0ff, emissiveIntensity: 0.5 });
const metalMat = new THREE.MeshStandardMaterial({ color: 0x374151, metalness: 0.95, roughness: 0.1 });
// Gun Main Body
const bodyGeo = new THREE.BoxGeometry(0.12, 0.18, 0.6);
const body = new THREE.Mesh(bodyGeo, bodyMat);
body.position.set(0, 0, 0);
// Barrel
const barrelGeo = new THREE.CylinderGeometry(0.04, 0.04, 0.5, 16);
const barrel = new THREE.Mesh(barrelGeo, metalMat);
barrel.rotation.x = Math.PI / 2;
barrel.position.set(0, 0.04, -0.4);
// Energy Cell / Scope Accent
const scopeGeo = new THREE.BoxGeometry(0.08, 0.06, 0.25);
const scope = new THREE.Mesh(scopeGeo, accentMat);
scope.position.set(0, 0.12, -0.05);
// Muzzle Flash Point Light
this.muzzleFlashLight = new THREE.PointLight(0x00f0ff, 0, 8);
this.muzzleFlashLight.position.set(0, 0.04, -0.7);
this.gunGroup.add(body, barrel, scope, this.muzzleFlashLight);
// Offset Gun to bottom-right of player view camera
this.gunGroup.position.set(0.28, -0.24, -0.45);
this.camera.add(this.gunGroup);
this.scene.add(this.camera);
},
setupEvents() {
// Key state trackers
window.addEventListener('keydown', (e) => {
this.keys[e.code] = true;
if (e.code === 'KeyR') this.reloadWeapon();
});
window.addEventListener('keyup', (e) => {
this.keys[e.code] = false;
});
// Mouse Movement for Pointer Lock Controls
window.addEventListener('mousemove', (e) => {
if (document.pointerLockElement !== document.body) return;
const sensitivity = 0.0022;
this.player.yaw -= e.movementX * sensitivity;
this.player.pitch -= e.movementY * sensitivity;
// Clamp pitch vertical look angle (-89 deg to +89 deg)
const maxPitch = Math.PI / 2 - 0.02;
this.player.pitch = Math.max(-maxPitch, Math.min(maxPitch, this.player.pitch));
});
// Shooting Mouse Click
window.addEventListener('mousedown', (e) => {
if (document.pointerLockElement === document.body && e.button === 0) {
this.shoot();
}
});
// Pointer Lock State Change Listeners
document.addEventListener('pointerlockchange', () => {
if (document.pointerLockElement === document.body) {
if (this.started && !this.gameOver) {
this.paused = false;
document.getElementById('pause-screen').classList.add('hidden');
document.getElementById('hud').classList.remove('hidden');
document.getElementById('crosshair').classList.remove('hidden');
}
} else {
if (this.started && !this.gameOver) {
this.paused = true;
document.getElementById('pause-screen').classList.remove('hidden');
document.getElementById('hud').classList.add('hidden');
document.getElementById('crosshair').classList.add('hidden');
}
}
});
// UI Button Actions
document.getElementById('btn-start').addEventListener('click', () => {
sfx.init();
this.startGame();
});
document.getElementById('btn-resume').addEventListener('click', () => {
document.body.requestPointerLock();
});
document.getElementById('btn-restart').addEventListener('click', () => {
this.restartGame();
});
},
startGame() {
this.started = true;
this.paused = false;
this.gameOver = false;
this.score = 0;
this.wave = 1;
this.health = 100;
this.ammo = 30;
this.updateHUD();
document.getElementById('start-screen').classList.add('hidden');
document.getElementById('gameover-screen').classList.add('hidden');
document.getElementById('hud').classList.remove('hidden');
document.getElementById('crosshair').classList.remove('hidden');
document.body.requestPointerLock();
this.spawnWave();
},
restartGame() {
// Clear existing active enemies
this.enemies.forEach(e => this.scene.remove(e.mesh));
this.enemies = [];
this.bullets.forEach(b => this.scene.remove(b.mesh));
this.bullets = [];
this.particles.forEach(p => this.scene.remove(p.mesh));
this.particles = [];
// Reset player transform
this.player.position.set(0, 1.7, 0);
this.player.velocity.set(0, 0, 0);
this.player.yaw = 0;
this.player.pitch = 0;
this.startGame();
},
spawnWave() {
const count = 4 + (this.wave - 1) * 3;
document.getElementById('hud-wave').innerText = this.wave;
for (let i = 0; i < count; i++) {
this.spawnEnemy();
}
this.updateHUD();
},
spawnEnemy() {
// Enemy Robot Model Construction
const group = new THREE.Group();
// Core Torso Body
const bodyGeo = new THREE.BoxGeometry(1.2, 1.5, 0.8);
const bodyMat = new THREE.MeshStandardMaterial({
color: 0x991b1b,
roughness: 0.3,
metalness: 0.8
});
const body = new THREE.Mesh(bodyGeo, bodyMat);
body.position.y = 1.25;
body.castShadow = true;
group.add(body);
// Glowing Head Visor Eye
const headGeo = new THREE.BoxGeometry(0.8, 0.6, 0.7);
const headMat = new THREE.MeshStandardMaterial({ color: 0x111827 });
const head = new THREE.Mesh(headGeo, headMat);
head.position.y = 2.3;
group.add(head);
const eyeGeo = new THREE.BoxGeometry(0.6, 0.15, 0.1);
const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff0044 });
const eye = new THREE.Mesh(eyeGeo, eyeMat);
eye.position.set(0, 2.35, 0.36);
group.add(eye);
// Floating Jet / Hover Legs
const baseGeo = new THREE.CylinderGeometry(0.5, 0.1, 0.8, 8);
const baseMat = new THREE.MeshStandardMaterial({ color: 0x374151, metalness: 0.9 });
const base = new THREE.Mesh(baseGeo, baseMat);
base.position.y = 0.4;
group.add(base);
// Position enemy at safe distance away from player
let spawnX, spawnZ;
do {
spawnX = (Math.random() - 0.5) * (this.ARENA_SIZE - 12);
spawnZ = (Math.random() - 0.5) * (this.ARENA_SIZE - 12);
} while (Math.hypot(spawnX - this.player.position.x, spawnZ - this.player.position.z) < 15);
group.position.set(spawnX, 0, spawnZ);
this.scene.add(group);
// Enemy data record
const hp = 80 + (this.wave * 20);
this.enemies.push({
mesh: group,
bodyMesh: body,
health: hp,
maxHealth: hp,
speed: 3.5 + Math.min(this.wave * 0.4, 4),
attackCooldown: 0,
hitTimer: 0
});
},
shoot() {
if (this.isReloading) return;
if (this.ammo <= 0) {
this.reloadWeapon();
return;
}
this.ammo--;
this.updateHUD();
sfx.playGunshot();
// Muzzle Flash & Recoil Trigger
this.recoilAmount = 0.08;
this.muzzleFlashLight.intensity = 4;
setTimeout(() => { this.muzzleFlashLight.intensity = 0; }, 40);
// Calculate Bullet Trajectory Raycast
const rayDir = new THREE.Vector3(0, 0, -1).applyQuaternion(this.camera.quaternion).normalize();
const rayOrigin = this.camera.position.clone();
this.raycaster.set(rayOrigin, rayDir);
// Spawn Visual Tracer Line Beam
this.createTracer(rayOrigin, rayDir);
// Check Collisions with Enemies
const enemyMeshes = this.enemies.map(e => e.mesh);
const intersects = this.raycaster.intersectObjects(enemyMeshes, true);
if (intersects.length > 0) {
const hitObj = intersects[0].object;
let hitEnemy = this.enemies.find(e => e.mesh === hitObj || e.mesh.children.includes(hitObj));
if (!hitEnemy && hitObj.parent) {
hitEnemy = this.enemies.find(e => e.mesh === hitObj.parent);
}
if (hitEnemy) {
const damage = 35;
hitEnemy.health -= damage;
hitEnemy.hitTimer = 0.1; // Flash red/white duration
sfx.playHit();
// Spawn impact spark particles
this.createSparks(intersects[0].point, intersects[0].face.normal);
// Enemy Death Handling
if (hitEnemy.health <= 0) {
sfx.playExplosion();
this.createExplosion(hitEnemy.mesh.position.clone());
this.scene.remove(hitEnemy.mesh);
this.enemies = this.enemies.filter(e => e !== hitEnemy);
this.score += 150;
this.updateHUD();
// Check Wave Cleared Condition
if (this.enemies.length === 0) {
this.wave++;
setTimeout(() => this.spawnWave(), 1500);
}
}
}
}
},
reloadWeapon() {
if (this.isReloading || this.ammo === this.maxAmmo) return;
this.isReloading = true;
sfx.playReload();
document.getElementById('reload-msg').classList.remove('hidden');
setTimeout(() => {
this.ammo = this.maxAmmo;
this.isReloading = false;
document.getElementById('reload-msg').classList.add('hidden');
this.updateHUD();
}, 1200);
},
createTracer(origin, direction) {
const tracerMat = new THREE.LineBasicMaterial({ color: 0x00f0ff, transparent: true, opacity: 0.8 });
const points = [];
// Start slightly in front of camera
const start = origin.clone().add(direction.clone().multiplyScalar(0.5));
const end = origin.clone().add(direction.clone().multiplyScalar(60));
points.push(start, end);
const tracerGeo = new THREE.BufferGeometry().setFromPoints(points);
const line = new THREE.Line(tracerGeo, tracerMat);
this.scene.add(line);
// Fade and destroy tracer
let alpha = 0.8;
const fade = () => {
alpha -= 0.1;
if (alpha <= 0) {
this.scene.remove(line);
} else {
tracerMat.opacity = alpha;
requestAnimationFrame(fade);
}
};
fade();
},
createSparks(position, normal) {
const particleCount = 8;
const geo = new THREE.BufferGeometry();
const positions = [];
const velocities = [];
for (let i = 0; i < particleCount; i++) {
positions.push(position.x, position.y, position.z);
const vel = normal.clone().add(new THREE.Vector3(
(Math.random() - 0.5) * 1.5,
(Math.random() - 0.5) * 1.5,
(Math.random() - 0.5) * 1.5
)).normalize().multiplyScalar(5 + Math.random() * 5);
velocities.push(vel);
}
geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
const mat = new THREE.PointsMaterial({ color: 0x00f0ff, size: 0.15, transparent: true });
const pSystem = new THREE.Points(geo, mat);
this.scene.add(pSystem);
this.particles.push({ mesh: pSystem, velocities: velocities, life: 0.25 });
},
createExplosion(position) {
const count = 25;
const geo = new THREE.BufferGeometry();
const positions = [];
const velocities = [];
for (let i = 0; i < count; i++) {
positions.push(position.x, position.y + 1, position.z);
const vel = new THREE.Vector3(
(Math.random() - 0.5) * 8,
Math.random() * 8,
(Math.random() - 0.5) * 8
);
velocities.push(vel);
}
geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
const mat = new THREE.PointsMaterial({ color: 0xef4444, size: 0.35, transparent: true });
const pSystem = new THREE.Points(geo, mat);
this.scene.add(pSystem);
this.particles.push({ mesh: pSystem, velocities: velocities, life: 0.5 });
},
takeDamage(amount) {
this.health = Math.max(0, this.health - amount);
this.updateHUD();
sfx.playPlayerHit();
// Flash Red Vignette HUD overlay
const overlay = document.getElementById('damage-overlay');
overlay.style.opacity = '1';
setTimeout(() => { overlay.style.opacity = '0'; }, 200);
if (this.health <= 0 && !this.gameOver) {
this.triggerGameOver();
}
},
triggerGameOver() {
this.gameOver = true;
document.exitPointerLock();
document.getElementById('hud').classList.add('hidden');
document.getElementById('crosshair').classList.add('hidden');
document.getElementById('final-score').innerText = this.score;
document.getElementById('final-wave').innerText = `Survived until Wave ${this.wave}`;
document.getElementById('gameover-screen').classList.remove('hidden');
},
updateHUD() {
document.getElementById('hud-score').innerText = this.score;
document.getElementById('hud-health-num').innerText = `${Math.ceil(this.health)} HP`;
document.getElementById('hud-health-bar').style.width = `${(this.health / this.maxHealth) * 100}%`;
document.getElementById('hud-ammo').innerText = this.ammo;
document.getElementById('hud-enemies').innerText = this.enemies.length;
},
updatePlayer(delta) {
// Apply Camera Pitch / Yaw Rotation
this.camera.rotation.x = this.player.pitch;
this.camera.rotation.y = this.player.yaw;
// Ground Check & Gravity Acceleration
if (!this.player.onGround) {
this.player.velocity.y -= 25 * delta; // Gravity
}
// Input Movement Calculation
const moveVector = new THREE.Vector3();
if (this.keys['KeyW'] || this.keys['ArrowUp']) moveVector.z -= 1;
if (this.keys['KeyS'] || this.keys['ArrowDown']) moveVector.z += 1;
if (this.keys['KeyA'] || this.keys['ArrowLeft']) moveVector.x -= 1;
if (this.keys['KeyD'] || this.keys['ArrowRight']) moveVector.x += 1;
moveVector.normalize();
// Transform movement vector into camera relative direction (Horizontal plane)
const euler = new THREE.Euler(0, this.player.yaw, 0, 'YXZ');
moveVector.applyEuler(euler);
// Velocity update
this.player.velocity.x = moveVector.x * this.player.speed;
this.player.velocity.z = moveVector.z * this.player.speed;
// Jump Action
if (this.keys['Space'] && this.player.onGround) {
this.player.velocity.y = this.player.jumpForce;
this.player.onGround = false;
}
// Potential Future Position
const nextPosition = this.player.position.clone().add(
new THREE.Vector3(this.player.velocity.x * delta, this.player.velocity.y * delta, this.player.velocity.z * delta)
);
// Collision Detection with Obstacles & Arena Boundaries
const playerBoundingBox = new THREE.Box3(
new THREE.Vector3(nextPosition.x - this.PLAYER_RADIUS, nextPosition.y - 1.7, nextPosition.z - this.PLAYER_RADIUS),
new THREE.Vector3(nextPosition.x + this.PLAYER_RADIUS, nextPosition.y + 0.3, nextPosition.z + this.PLAYER_RADIUS)
);
let collided = false;
for (const obstacleBox of this.obstacles) {
if (playerBoundingBox.intersectsBox(obstacleBox)) {
collided = true;
break;
}
}
if (!collided) {
this.player.position.copy(nextPosition);
} else {
// Slide along axes if full collision blocked
this.player.position.y += this.player.velocity.y * delta;
}
// Floor height clamp
if (this.player.position.y <= 1.7) {
this.player.position.y = 1.7;
this.player.velocity.y = 0;
this.player.onGround = true;
}
// Sync Three.js Camera Position
this.camera.position.copy(this.player.position);
// Weapon Recoil Animation Spring Return
if (this.recoilAmount > 0) {
this.gunGroup.position.z = -0.45 + this.recoilAmount;
this.recoilAmount = Math.max(0, this.recoilAmount - delta * 0.5);
} else {
this.gunGroup.position.z = -0.45;
}
// Gun Bobbing effect while moving
if (moveVector.lengthSq() > 0 && this.player.onGround) {
const time = this.clock.getElapsedTime() * 10;
this.gunGroup.position.y = -0.24 + Math.sin(time) * 0.015;
this.gunGroup.position.x = 0.28 + Math.cos(time * 0.5) * 0.015;
}
},
updateEnemies(delta) {
this.enemies.forEach(enemy => {
// Flash hit animation material restore
if (enemy.hitTimer > 0) {
enemy.hitTimer -= delta;
enemy.bodyMesh.material.color.setHex(0xffffff);
} else {
enemy.bodyMesh.material.color.setHex(0x991b1b);
}
// Move enemy vector towards player
const dir = new THREE.Vector3().subVectors(this.player.position, enemy.mesh.position);
dir.y = 0; // Lock to floor plane
const dist = dir.length();
dir.normalize();
enemy.mesh.rotation.y = Math.atan2(dir.x, dir.z);
// Move if not right inside attack range
if (dist > 1.8) {
enemy.mesh.position.add(dir.multiplyScalar(enemy.speed * delta));
} else {
// Melee Attack Player
if (enemy.attackCooldown <= 0) {
this.takeDamage(15);
enemy.attackCooldown = 1.2; // Attack delay seconds
}
}
if (enemy.attackCooldown > 0) {
enemy.attackCooldown -= delta;
}
});
},
updateParticles(delta) {
for (let i = this.particles.length - 1; i >= 0; i--) {
const p = this.particles[i];
p.life -= delta;
if (p.life <= 0) {
this.scene.remove(p.mesh);
this.particles.splice(i, 1);
continue;
}
const positions = p.mesh.geometry.attributes.position.array;
for (let j = 0; j < p.velocities.length; j++) {
positions[j * 3] += p.velocities[j].x * delta;
positions[j * 3 + 1] += p.velocities[j].y * delta;
positions[j * 3 + 2] += p.velocities[j].z * delta;
}
p.mesh.geometry.attributes.position.needsUpdate = true;
}
},
onWindowResize() {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
},
// Main Animation Loop
animate() {
requestAnimationFrame(() => this.animate());
const delta = Math.min(this.clock.getDelta(), 0.1);
if (this.started && !this.paused && !this.gameOver) {
this.updatePlayer(delta);
this.updateEnemies(delta);
this.updateParticles(delta);
}
this.renderer.render(this.scene, this.camera);
}
};
// Launch Game on window initialization
window.onload = () => {
Game.init();
Game.animate();
};
</script>
</body>
</html>