<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>3D Tank Warzone</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Three.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&family=Rajdhani:wght@600;700&display=swap" rel="stylesheet">
<style>
* {
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
touch-action: none;
}
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: #050811;
font-family: 'Rajdhani', sans-serif;
}
.font-orbitron {
font-family: 'Orbitron', sans-serif;
}
.glass-panel {
background: rgba(10, 16, 28, 0.85);
backdrop-filter: blur(8px);
border: 1px solid rgba(0, 255, 204, 0.3);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6);
}
.glow-text {
text-shadow: 0 0 10px rgba(0, 255, 204, 0.7);
}
.joystick-base {
position: relative;
width: 120px;
height: 120px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.4);
border: 2px solid rgba(0, 255, 204, 0.4);
}
.joystick-knob {
position: absolute;
top: 50%;
left: 50%;
width: 44px;
height: 44px;
margin-top: -22px;
margin-left: -22px;
border-radius: 50%;
background: rgba(0, 255, 204, 0.8);
box-shadow: 0 0 12px rgba(0, 255, 204, 0.8);
pointer-events: none;
}
</style>
</head>
<body class="w-full h-full relative select-none">
<!-- 3D Canvas -->
<div id="game-canvas-container" class="w-full h-full absolute inset-0 z-0"></div>
<!-- HUD Overlay -->
<div id="hud-layer" class="absolute inset-0 z-10 pointer-events-none flex flex-col justify-between p-4">
<!-- Top HUD -->
<div class="flex justify-between items-start w-full">
<!-- Health Card -->
<div class="glass-panel p-3 rounded-xl w-60 md:w-72 pointer-events-auto">
<div class="flex justify-between items-center mb-1">
<span class="font-orbitron text-xs font-bold text-cyan-400">BLINDAJE</span>
<span id="health-val" class="font-orbitron text-xs font-bold text-emerald-400">100 HP</span>
</div>
<div class="w-full bg-gray-900 h-3 rounded-full overflow-hidden border border-gray-700">
<div id="health-bar" class="bg-gradient-to-r from-emerald-500 to-cyan-400 h-full w-full transition-all duration-150"></div>
</div>
<div class="flex justify-between items-center mt-2 mb-0.5">
<span class="font-orbitron text-[10px] text-gray-400">RECARGA CAÑÓN</span>
<span id="reload-val" class="font-orbitron text-[10px] text-emerald-400">LISTO</span>
</div>
<div class="w-full bg-gray-900 h-1.5 rounded-full overflow-hidden">
<div id="reload-bar" class="bg-cyan-400 h-full w-full transition-all duration-75"></div>
</div>
</div>
<!-- Stats -->
<div class="glass-panel px-4 py-2 rounded-xl flex items-center space-x-4 text-center pointer-events-auto">
<div>
<div class="text-[10px] text-gray-400 font-orbitron">OLEADA</div>
<div id="hud-wave" class="text-xl font-black font-orbitron text-cyan-400 glow-text">1</div>
</div>
<div class="h-6 w-px bg-cyan-500/30"></div>
<div>
<div class="text-[10px] text-gray-400 font-orbitron">BAJAS</div>
<div id="hud-kills" class="text-xl font-black font-orbitron text-amber-400">0</div>
</div>
<div class="h-6 w-px bg-cyan-500/30"></div>
<div>
<div class="text-[10px] text-gray-400 font-orbitron">PUNTOS</div>
<div id="hud-score" class="text-xl font-black font-orbitron text-white">0</div>
</div>
</div>
<!-- Speed Control Sliders Panel -->
<div class="glass-panel p-3 rounded-xl w-52 md:w-64 pointer-events-auto flex flex-col space-y-2">
<div class="text-xs font-bold font-orbitron text-cyan-400 mb-1">AJUSTES DE VELOCIDAD</div>
<div>
<div class="flex justify-between items-center text-[10px] text-gray-300 font-orbitron mb-1">
<span>MI VELOCIDAD</span>
<span id="player-speed-val" class="text-emerald-400 font-bold">100%</span>
</div>
<input id="slider-player-speed" type="range" min="0.5" max="2.5" step="0.1" value="1.0"
class="w-full h-1.5 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-emerald-400">
</div>
<div>
<div class="flex justify-between items-center text-[10px] text-gray-300 font-orbitron mb-1">
<span>VELOCIDAD ENEMIGOS</span>
<span id="enemy-speed-val" class="text-amber-400 font-bold">40%</span>
</div>
<input id="slider-enemy-speed" type="range" min="0.1" max="1.5" step="0.05" value="0.4"
class="w-full h-1.5 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-amber-400">
</div>
</div>
<!-- Radar Minimap -->
<div class="glass-panel p-1 rounded-xl w-28 h-28 md:w-32 md:h-32 pointer-events-auto relative">
<canvas id="radar-canvas" width="120" height="120" class="w-full h-full rounded-lg"></canvas>
</div>
</div>
<!-- Touch Controls -->
<div id="touch-controls" class="w-full flex justify-between items-end pb-2 pointer-events-auto">
<!-- Left Stick: Steering -->
<div id="stick-move" class="joystick-base">
<div id="knob-move" class="joystick-knob"></div>
</div>
<!-- Fire Button -->
<button id="btn-fire" class="w-20 h-20 rounded-full bg-gradient-to-tr from-red-600 to-amber-500 border-2 border-red-300 font-orbitron font-black text-white text-sm active:scale-95 shadow-lg shadow-red-600/50 flex items-center justify-center pointer-events-auto">
DISPARAR
</button>
</div>
</div>
<!-- Start Overlay -->
<div id="screen-start" class="absolute inset-0 z-20 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
<div class="glass-panel p-8 rounded-2xl max-w-md w-full text-center border-cyan-500/50">
<h1 class="font-orbitron text-4xl font-black text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-emerald-400 mb-2 glow-text">
3D TANK WARZONE
</h1>
<p class="text-gray-300 text-sm mb-6">Usa WASD/Flechas para conducir, ratón para apuntar la torreta y Clic/Espacio para disparar.</p>
<button id="btn-start-game" class="w-full py-4 rounded-xl bg-gradient-to-r from-cyan-500 to-emerald-500 hover:from-cyan-400 hover:to-emerald-400 font-orbitron font-black text-lg text-slate-950 shadow-lg shadow-cyan-500/40 transition-all transform hover:scale-105 active:scale-95">
¡JUGAR AHORA!
</button>
</div>
</div>
<!-- Game Over Overlay -->
<div id="screen-gameover" class="absolute inset-0 z-20 flex items-center justify-center bg-black/85 backdrop-blur-md p-4 hidden">
<div class="glass-panel p-8 rounded-2xl max-w-md w-full text-center border-red-500/50">
<h2 class="font-orbitron text-3xl font-black text-red-500 mb-2 glow-text">TANQUE DESTRUIDO</h2>
<p class="text-gray-400 text-sm mb-6">Tu blindaje ha sucumbido al fuego enemigo.</p>
<div class="bg-gray-900/80 rounded-xl p-4 mb-6 border border-gray-800 space-y-2 text-sm font-orbitron">
<div class="flex justify-between">
<span class="text-gray-400">PUNTUACIÓN:</span>
<span id="final-score" class="text-cyan-400 font-bold">0</span>
</div>
<div class="flex justify-between">
<span class="text-gray-400">BAJAS:</span>
<span id="final-kills" class="text-amber-400 font-bold">0</span>
</div>
</div>
<button id="btn-restart-game" class="w-full py-4 rounded-xl bg-gradient-to-r from-red-600 to-amber-500 font-orbitron font-black text-lg text-white shadow-lg shadow-red-600/40 transition-all transform hover:scale-105 active:scale-95">
REPLEGAR TANQUE
</button>
</div>
</div>
<script>
// --- Web Audio Synthesizer ---
class SoundEngine {
constructor() { this.ctx = null; }
init() {
if (!this.ctx) {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
}
}
shoot() {
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(140, now);
osc.frequency.exponentialRampToValueAtTime(30, now + 0.3);
gain.gain.setValueAtTime(0.5, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
osc.connect(gain); gain.connect(this.ctx.destination);
osc.start(now); osc.stop(now + 0.3);
}
explode() {
if (!this.ctx) return;
const now = this.ctx.currentTime;
const bufSize = this.ctx.sampleRate * 0.4;
const buf = this.ctx.createBuffer(1, bufSize, this.ctx.sampleRate);
const data = buf.getChannelData(0);
for (let i = 0; i < bufSize; i++) data[i] = Math.random() * 2 - 1;
const src = this.ctx.createBufferSource();
src.buffer = buf;
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(600, now);
filter.frequency.exponentialRampToValueAtTime(40, now + 0.4);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.8, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.4);
src.connect(filter); filter.connect(gain); gain.connect(this.ctx.destination);
src.start(now);
}
}
const sfx = new SoundEngine();
// --- Game Logic ---
let scene, camera, renderer;
let playerTank, playerBody, playerTurret, playerBarrel;
let obstacles = [], enemies = [], projectiles = [], particles = [], wrecks = [];
let isGameRunning = false;
let health = 100, score = 0, kills = 0, wave = 1;
let canShoot = true, reloadProgress = 1;
// Speed Multipliers from Sliders
let playerSpeedMult = 1.0;
let enemySpeedMult = 0.4;
const keys = {};
const mouse = { x: 0, y: 0, worldPos: new THREE.Vector3() };
const raycaster = new THREE.Raycaster();
const groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
function init() {
const container = document.getElementById('game-canvas-container');
// 1. Scene & Camera
scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0f1d);
scene.fog = new THREE.FogExp2(0x0a0f1d, 0.012);
camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 1000);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
container.appendChild(renderer.domElement);
// 2. Lights
const ambient = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambient);
const sun = new THREE.DirectionalLight(0xffffff, 0.8);
sun.position.set(40, 80, 40);
sun.castShadow = true;
sun.shadow.mapSize.width = 2048;
sun.shadow.mapSize.height = 2048;
sun.shadow.camera.left = -70; sun.shadow.camera.right = 70;
sun.shadow.camera.top = 70; sun.shadow.camera.bottom = -70;
scene.add(sun);
// 3. Ground (Military Green 0x2e7d32 with Light Green Grid 0x81c784)
const groundGeo = new THREE.PlaneGeometry(240, 240);
const groundMat = new THREE.MeshStandardMaterial({ color: 0x2e7d32, roughness: 0.85, metalness: 0.1 });
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
const grid = new THREE.GridHelper(240, 48, 0x81c784, 0x388e3c);
grid.position.y = 0.02;
scene.add(grid);
// 4. Map Obstacles
buildMap();
// 5. Create Player Tank
playerTank = createTankMesh(0x00ffcc, 0x008877);
playerBody = playerTank.children[0];
playerTurret = playerTank.children[1];
playerBarrel = playerTurret.children[1];
scene.add(playerTank);
// 6. Listeners
window.addEventListener('resize', onWindowResize);
window.addEventListener('keydown', e => { keys[e.code] = true; });
window.addEventListener('keyup', e => { keys[e.code] = false; });
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mousedown', e => {
if (e.button === 0 && isGameRunning && canShoot) shootCannon(playerTank, true);
});
setupTouchControls();
// 7. Start Render Loop
requestAnimationFrame(animate);
}
function createTankMesh(chassisColor, turretColor) {
const tank = new THREE.Group();
// Body / Chassis
const bodyGeo = new THREE.BoxGeometry(3.6, 1.2, 5.0);
const bodyMat = new THREE.MeshStandardMaterial({ color: chassisColor, roughness: 0.4 });
const body = new THREE.Mesh(bodyGeo, bodyMat);
body.position.y = 0.8;
body.castShadow = true;
body.receiveShadow = true;
tank.add(body);
// Turret Group
const turretGroup = new THREE.Group();
turretGroup.position.set(0, 1.6, 0);
const turretGeo = new THREE.CylinderGeometry(1.3, 1.5, 0.9, 8);
const turretMat = new THREE.MeshStandardMaterial({ color: turretColor, roughness: 0.3 });
const turret = new THREE.Mesh(turretGeo, turretMat);
turret.castShadow = true;
turretGroup.add(turret);
// Barrel
const barrelGeo = new THREE.CylinderGeometry(0.2, 0.25, 3.2, 12);
const barrelMat = new THREE.MeshStandardMaterial({ color: 0x222222, metalness: 0.8 });
const barrel = new THREE.Mesh(barrelGeo, barrelMat);
barrel.rotation.x = Math.PI / 2;
barrel.position.set(0, 0.1, 1.6);
barrel.castShadow = true;
turretGroup.add(barrel);
tank.add(turretGroup);
return tank;
}
function buildMap() {
const wallMat = new THREE.MeshStandardMaterial({ color: 0x1e293b, roughness: 0.6 });
// Outer Walls
const hWall = new THREE.BoxGeometry(240, 5, 3);
const vWall = new THREE.BoxGeometry(3, 5, 240);
const n = new THREE.Mesh(hWall, wallMat); n.position.set(0, 2.5, -120); scene.add(n); obstacles.push(n);
const s = new THREE.Mesh(hWall, wallMat); s.position.set(0, 2.5, 120); scene.add(s); obstacles.push(s);
const e = new THREE.Mesh(vWall, wallMat); e.position.set(120, 2.5, 0); scene.add(e); obstacles.push(e);
const w = new THREE.Mesh(vWall, wallMat); w.position.set(-120, 2.5, 0); scene.add(w); obstacles.push(w);
// Inner Obstacles
const boxGeo = new THREE.BoxGeometry(1, 1, 1);
const positions = [
{x: -30, z: -30, w: 10, h: 5, d: 10},
{x: 30, z: -30, w: 12, h: 5, d: 8},
{x: -30, z: 30, w: 8, h: 5, d: 12},
{x: 30, z: 30, w: 14, h: 5, d: 10},
{x: 0, z: -60, w: 16, h: 6, d: 8},
{x: 0, z: 60, w: 16, h: 6, d: 8}
];
positions.forEach(p => {
const obs = new THREE.Mesh(boxGeo, wallMat);
obs.scale.set(p.w, p.h, p.d);
obs.position.set(p.x, p.h / 2, p.z);
obs.castShadow = true;
obs.receiveShadow = true;
scene.add(obs);
obs.radius = Math.max(p.w, p.d) / 2;
obstacles.push(obs);
});
}
function spawnWave() {
const count = 2 + wave * 2;
for (let i = 0; i < count; i++) {
let rx, rz;
do {
rx = (Math.random() - 0.5) * 180;
rz = (Math.random() - 0.5) * 180;
} while (Math.hypot(rx - playerTank.position.x, rz - playerTank.position.z) < 35);
const enemy = createTankMesh(0xff3366, 0x990033);
enemy.position.set(rx, 0, rz);
enemy.userData = {
health: 40 + wave * 10,
maxHealth: 40 + wave * 10,
lastShoot: 0,
shootInterval: Math.max(1400, 2600 - wave * 150),
speed: 0.08 + Math.min(0.06, wave * 0.008)
};
scene.add(enemy);
enemies.push(enemy);
}
}
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
raycaster.ray.intersectPlane(groundPlane, mouse.worldPos);
}
function shootCannon(tank, isPlayer = false) {
if (isPlayer && !canShoot) return;
const turret = tank.children[1];
const barrel = turret.children[1];
const muzzlePos = new THREE.Vector3();
barrel.getWorldPosition(muzzlePos);
const dir = new THREE.Vector3();
turret.getWorldDirection(dir);
dir.y = 0;
dir.normalize();
// Shell
const projGeo = new THREE.SphereGeometry(0.35, 8, 8);
const projMat = new THREE.MeshBasicMaterial({ color: isPlayer ? 0x00ffcc : 0xffaa00 });
const proj = new THREE.Mesh(projGeo, projMat);
proj.position.copy(muzzlePos);
proj.userData = { dir: dir, speed: 1.1, isPlayer: isPlayer, life: 100 };
scene.add(proj);
projectiles.push(proj);
sfx.shoot();
if (isPlayer) {
canShoot = false;
reloadProgress = 0;
document.getElementById('reload-val').innerText = 'RECARGANDO';
document.getElementById('reload-val').className = 'font-orbitron text-[10px] text-amber-400';
}
}
function createExplosion(pos, color = 0xffaa00, count = 20) {
sfx.explode();
for (let i = 0; i < count; i++) {
const pGeo = new THREE.BoxGeometry(0.3, 0.3, 0.3);
const pMat = new THREE.MeshBasicMaterial({ color: color });
const p = new THREE.Mesh(pGeo, pMat);
p.position.copy(pos);
p.userData = {
vel: new THREE.Vector3((Math.random() - 0.5) * 0.6, Math.random() * 0.5 + 0.2, (Math.random() - 0.5) * 0.6),
life: 25 + Math.random() * 15
};
scene.add(p);
particles.push(p);
}
}
function setupTouchControls() {
const moveZone = document.getElementById('stick-move');
const moveKnob = document.getElementById('knob-move');
const fireBtn = document.getElementById('btn-fire');
// Sliders setup
const sliderPlayer = document.getElementById('slider-player-speed');
const sliderEnemy = document.getElementById('slider-enemy-speed');
sliderPlayer.addEventListener('input', (e) => {
playerSpeedMult = parseFloat(e.target.value);
document.getElementById('player-speed-val').innerText = Math.round(playerSpeedMult * 100) + '%';
});
sliderEnemy.addEventListener('input', (e) => {
enemySpeedMult = parseFloat(e.target.value);
document.getElementById('enemy-speed-val').innerText = Math.round(enemySpeedMult * 100) + '%';
});
let activeId = null;
let startX = 0, startY = 0;
moveZone.addEventListener('touchstart', e => {
const t = e.changedTouches[0];
activeId = t.identifier;
startX = t.clientX; startY = t.clientY;
});
moveZone.addEventListener('touchmove', e => {
for (let t of e.changedTouches) {
if (t.identifier === activeId) {
const dx = t.clientX - startX;
const dy = t.clientY - startY;
const dist = Math.min(40, Math.hypot(dx, dy));
const angle = Math.atan2(dy, dx);
moveKnob.style.transform = `translate(${Math.cos(angle)*dist}px, ${Math.sin(angle)*dist}px)`;
keys['KeyW'] = dy < -10;
keys['KeyS'] = dy > 10;
keys['KeyA'] = dx < -10;
keys['KeyD'] = dx > 10;
}
}
});
const resetTouch = () => {
moveKnob.style.transform = `translate(0px, 0px)`;
keys['KeyW'] = false; keys['KeyS'] = false;
keys['KeyA'] = false; keys['KeyD'] = false;
};
moveZone.addEventListener('touchend', resetTouch);
moveZone.addEventListener('touchcancel', resetTouch);
fireBtn.addEventListener('click', () => {
if (isGameRunning && canShoot) shootCannon(playerTank, true);
});
}
function updateRadar() {
const canvas = document.getElementById('radar-canvas');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, 120, 120);
ctx.fillStyle = 'rgba(5, 15, 25, 0.8)';
ctx.fillRect(0, 0, 120, 120);
const c = 60;
const scale = 0.5;
// Player
ctx.fillStyle = '#00ffcc';
ctx.beginPath();
ctx.arc(c, c, 3.5, 0, Math.PI * 2);
ctx.fill();
// Enemies
ctx.fillStyle = '#ff3366';
enemies.forEach(e => {
const dx = (e.position.x - playerTank.position.x) * scale;
const dz = (e.position.z - playerTank.position.z) * scale;
if (Math.hypot(dx, dz) < 55) {
ctx.beginPath();
ctx.arc(c + dx, c + dz, 3, 0, Math.PI * 2);
ctx.fill();
}
});
}
function updateGameLogic() {
if (!isGameRunning) return;
// 1. Reload Bar
if (!canShoot) {
reloadProgress += 0.025;
document.getElementById('reload-bar').style.width = (reloadProgress * 100) + '%';
if (reloadProgress >= 1) {
canShoot = true;
document.getElementById('reload-val').innerText = 'LISTO';
document.getElementById('reload-val').className = 'font-orbitron text-[10px] text-emerald-400';
}
}
// 2. Player Steering (Scaled by Speed Slider)
const moveSpeed = 0.18 * playerSpeedMult;
const rotSpeed = 0.04 * Math.sqrt(playerSpeedMult);
if (keys['KeyW'] || keys['ArrowUp']) playerTank.translateZ(moveSpeed);
if (keys['KeyS'] || keys['ArrowDown']) playerTank.translateZ(-moveSpeed);
if (keys['KeyA'] || keys['ArrowLeft']) playerTank.rotation.y += rotSpeed;
if (keys['KeyD'] || keys['ArrowRight']) playerTank.rotation.y -= rotSpeed;
playerTank.position.x = Math.max(-115, Math.min(115, playerTank.position.x));
playerTank.position.z = Math.max(-115, Math.min(115, playerTank.position.z));
// Turret Mouse Aiming
playerTurret.lookAt(mouse.worldPos);
// Camera Tracking
camera.position.x = playerTank.position.x;
camera.position.z = playerTank.position.z + 24;
camera.position.y = playerTank.position.y + 18;
camera.lookAt(playerTank.position);
// 3. Enemies AI (Scaled by Enemy Speed Slider)
const now = Date.now();
enemies.forEach(enemy => {
const dist = enemy.position.distanceTo(playerTank.position);
enemy.lookAt(playerTank.position);
if (dist > 14) {
enemy.translateZ(enemy.userData.speed * enemySpeedMult);
}
if (dist < 50 && now - enemy.userData.lastShoot > enemy.userData.shootInterval) {
shootCannon(enemy, false);
enemy.userData.lastShoot = now;
}
});
// 4. Projectiles Update
for (let i = projectiles.length - 1; i >= 0; i--) {
const p = projectiles[i];
p.position.addScaledVector(p.userData.dir, p.userData.speed);
p.userData.life--;
let hit = false;
// Hit Obstacles
obstacles.forEach(obs => {
const box = new THREE.Box3().setFromObject(obs);
if (box.containsPoint(p.position)) {
createExplosion(p.position, 0xaaaaaa, 8);
hit = true;
}
});
// Hit Enemies
if (!hit && p.userData.isPlayer) {
enemies.forEach((enemy, eIdx) => {
if (p.position.distanceTo(enemy.position) < 2.5) {
enemy.userData.health -= 25;
createExplosion(p.position, 0xff9900, 14);
hit = true;
if (enemy.userData.health <= 0) {
createExplosion(enemy.position, 0xff3300, 30);
// Wreckage
const wreck = new THREE.Mesh(
new THREE.BoxGeometry(3.6, 1, 4.8),
new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.9 })
);
wreck.position.copy(enemy.position);
scene.add(wreck);
wrecks.push(wreck);
scene.remove(enemy);
enemies.splice(eIdx, 1);
kills++;
score += 150;
document.getElementById('hud-kills').innerText = kills;
document.getElementById('hud-score').innerText = score;
if (enemies.length === 0) {
wave++;
document.getElementById('hud-wave').innerText = wave;
health = Math.min(100, health + 25);
updateHealthUI();
spawnWave();
}
}
}
});
}
// Hit Player
if (!hit && !p.userData.isPlayer) {
if (p.position.distanceTo(playerTank.position) < 2.2) {
health -= 15;
updateHealthUI();
createExplosion(p.position, 0xff0033, 18);
hit = true;
if (health <= 0) {
isGameRunning = false;
createExplosion(playerTank.position, 0xff0000, 40);
document.getElementById('final-score').innerText = score;
document.getElementById('final-kills').innerText = kills;
document.getElementById('screen-gameover').classList.remove('hidden');
}
}
}
if (hit || p.userData.life <= 0) {
scene.remove(p);
projectiles.splice(i, 1);
}
}
// 5. Particles Update
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.position.add(p.userData.vel);
p.userData.vel.y -= 0.015;
p.userData.life--;
if (p.userData.life <= 0) {
scene.remove(p);
particles.splice(i, 1);
}
}
updateRadar();
}
function updateHealthUI() {
document.getElementById('health-val').innerText = health + ' HP';
document.getElementById('health-bar').style.width = health + '%';
}
function resetAndStartGame() {
enemies.forEach(e => scene.remove(e));
projectiles.forEach(p => scene.remove(p));
particles.forEach(p => scene.remove(p));
wrecks.forEach(w => scene.remove(w));
enemies = []; projectiles = []; particles = []; wrecks = [];
health = 100; score = 0; kills = 0; wave = 1;
playerTank.position.set(0, 0, 0);
updateHealthUI();
document.getElementById('hud-wave').innerText = '1';
document.getElementById('hud-kills').innerText = '0';
document.getElementById('hud-score').innerText = '0';
spawnWave();
isGameRunning = true;
}
function animate() {
requestAnimationFrame(animate);
updateGameLogic();
renderer.render(scene, camera);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// Start & Restart Listeners
document.getElementById('btn-start-game').addEventListener('click', () => {
sfx.init();
document.getElementById('screen-start').classList.add('hidden');
resetAndStartGame();
});
document.getElementById('btn-restart-game').addEventListener('click', () => {
sfx.init();
document.getElementById('screen-gameover').classList.add('hidden');
resetAndStartGame();
});
window.onload = init;
</script>
</body>
</html>
