codigo completo en html:
<!DOCTYPE html>
<html lang="en">
<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@400;700;900&family=Rajdhani:wght@500;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Rajdhani', sans-serif;
user-select: none;
-webkit-user-select: none;
touch-action: none;
overflow: hidden;
background-color: #050508;
}
.font-orbitron {
font-family: 'Orbitron', sans-serif;
}
/* Custom UI Glassmorphism */
.glass-panel {
background: rgba(13, 17, 23, 0.85);
backdrop-filter: blur(10px);
border: 1px solid rgba(0, 255, 204, 0.25);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.6);
}
.glow-text-cyan {
text-shadow: 0 0 12px rgba(0, 255, 255, 0.8);
}
.glow-text-red {
text-shadow: 0 0 12px rgba(255, 50, 50, 0.8);
}
.glow-box-cyan {
box-shadow: 0 0 18px rgba(0, 255, 255, 0.4);
}
/* Virtual Joysticks for Touch */
.joystick-zone {
position: relative;
width: 130px;
height: 130px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
border: 2px solid rgba(0, 255, 204, 0.4);
touch-action: none;
}
.joystick-knob {
position: absolute;
top: 50%;
left: 50%;
width: 48px;
height: 48px;
margin-top: -24px;
margin-left: -24px;
border-radius: 50%;
background: radial-gradient(circle, rgba(0,255,204,0.9) 0%, rgba(0,150,255,0.7) 100%);
box-shadow: 0 0 12px rgba(0,255,204,0.6);
pointer-events: none;
}
</style>
</head>
<body class="w-screen h-screen m-0 p-0 overflow-hidden bg-black text-white select-none">
<!-- 3D Render Canvas Container -->
<div id="canvas-container" class="w-full h-full absolute inset-0 z-0"></div>
<!-- HUD Overlay -->
<div id="hud-overlay" class="absolute inset-0 z-10 pointer-events-none flex flex-col justify-between p-3 md:p-6 hidden">
<!-- Top Bar Header -->
<div class="flex justify-between items-start w-full">
<!-- Health & Reload Bar -->
<div class="glass-panel p-3 md:p-4 rounded-xl w-56 md:w-80 pointer-events-auto">
<div class="flex justify-between items-center mb-1">
<span class="font-orbitron text-[10px] md:text-xs font-bold text-cyan-400">BLINDAJE DEL TANQUE</span>
<span id="health-text" class="font-orbitron text-[10px] md:text-xs font-bold text-emerald-400">100 HP</span>
</div>
<!-- Health Bar Track -->
<div class="w-full bg-gray-900 h-2.5 md:h-3 rounded-full overflow-hidden border border-gray-700 mb-2">
<div id="health-bar" class="bg-gradient-to-r from-emerald-500 to-cyan-400 h-full w-full transition-all duration-200"></div>
</div>
<div class="flex justify-between items-center mb-1">
<span class="font-orbitron text-[10px] md:text-xs text-gray-400">RECARGA CAÑÓN</span>
<span id="reload-text" class="font-orbitron text-[10px] md:text-xs text-emerald-400">LISTO</span>
</div>
<!-- Reload Bar Track -->
<div class="w-full bg-gray-900 h-1.5 rounded-full overflow-hidden border border-gray-800">
<div id="reload-bar" class="bg-cyan-400 h-full w-full transition-all duration-75"></div>
</div>
</div>
<!-- Score & Waves -->
<div class="glass-panel px-4 py-2 md:px-6 md:py-3 rounded-xl flex items-center space-x-3 md:space-x-6 text-center pointer-events-auto">
<div>
<div class="text-[9px] md:text-xs text-gray-400 font-orbitron uppercase">OLEADA</div>
<div id="hud-wave" class="text-xl md:text-2xl font-black font-orbitron text-cyan-400 glow-text-cyan">1</div>
</div>
<div class="h-6 md:h-8 w-px bg-cyan-500/30"></div>
<div>
<div class="text-[9px] md:text-xs text-gray-400 font-orbitron uppercase">BAJAS</div>
<div id="hud-kills" class="text-xl md:text-2xl font-black font-orbitron text-amber-400">0</div>
</div>
<div class="h-6 md:h-8 w-px bg-cyan-500/30"></div>
<div>
<div class="text-[9px] md:text-xs text-gray-400 font-orbitron uppercase">PUNTOS</div>
<div id="hud-score" class="text-xl md:text-2xl font-black font-orbitron text-white">0</div>
</div>
</div>
<!-- Radar Minimap -->
<div class="glass-panel p-1.5 rounded-xl relative w-28 h-28 md:w-36 md:h-36 flex items-center justify-center border-cyan-500/40 pointer-events-auto">
<canvas id="minimap-canvas" width="130" height="130" class="rounded-lg w-full h-full"></canvas>
<div class="absolute text-[8px] md:text-[9px] font-orbitron text-cyan-400/80 top-1 left-2">RADAR</div>
</div>
</div>
<!-- Crosshair / Aiming Center Pointer -->
<div id="crosshair" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none">
<div class="w-8 h-8 border border-cyan-400/60 rounded-full flex items-center justify-center">
<div class="w-1.5 h-1.5 bg-cyan-400 rounded-full glow-box-cyan"></div>
</div>
</div>
<!-- Touch Controls (Mobile / On-screen Controls) -->
<div id="touch-controls" class="w-full flex justify-between items-end pb-2 px-2 pointer-events-auto md:hidden">
<!-- Left Joystick: Move -->
<div id="joystick-left" class="joystick-zone">
<div id="knob-left" class="joystick-knob"></div>
</div>
<!-- Fire Button -->
<button id="btn-fire" class="w-16 h-16 rounded-full bg-gradient-to-tr from-red-600 to-amber-500 border-2 border-red-300 font-orbitron font-black text-white text-xs active:scale-95 shadow-lg shadow-red-600/50 flex items-center justify-center">
FUEGO
</button>
<!-- Right Joystick: Aim Turret -->
<div id="joystick-right" class="joystick-zone">
<div id="knob-right" class="joystick-knob"></div>
</div>
</div>
</div>
<!-- Start / Main Menu Overlay -->
<div id="start-menu" class="absolute inset-0 z-20 flex items-center justify-center bg-black/85 backdrop-blur-md p-4">
<div class="glass-panel p-6 md:p-10 rounded-2xl max-w-lg w-full text-center border-cyan-500/40 glow-box-cyan">
<h1 class="font-orbitron text-3xl md:text-5xl font-black text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 via-teal-300 to-emerald-400 mb-3 glow-text-cyan">
TANK WARZONE 3D
</h1>
<p class="text-gray-300 text-xs md:text-sm mb-6">¡Destruye los tanques enemigos con proyectiles 3D, sobrevive a las oleadas y domina el terreno!</p>
<!-- Controls Guide -->
<div class="bg-gray-900/90 rounded-xl p-4 mb-6 text-left border border-gray-800 text-xs md:text-sm space-y-2">
<div class="font-orbitron font-bold text-cyan-400 mb-1 uppercase">Controles de Combate</div>
<div class="flex justify-between"><span class="text-gray-400">Mover / Girar Tanque:</span> <span class="text-white font-mono">Teclas WASD / Flechas</span></div>
<div class="flex justify-between"><span class="text-gray-400">Apuntar Torreta (360°):</span> <span class="text-white font-mono">Movimiento del Ratón</span></div>
<div class="flex justify-between"><span class="text-gray-400">Disparar Cañón:</span> <span class="text-white font-mono">Clic Izquierdo / Espacio</span></div>
<div class="flex justify-between"><span class="text-gray-400">Dispositivos Táctiles:</span> <span class="text-white font-mono">Joysticks Virtuales</span></div>
</div>
<button id="btn-start" class="w-full py-3.5 rounded-xl bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 font-orbitron font-black text-base md:text-lg text-white shadow-lg shadow-cyan-500/30 transition-all transform hover:scale-[1.02] active:scale-95">
INICIAR BATALLA
</button>
</div>
</div>
<!-- Game Over Screen Overlay -->
<div id="gameover-menu" class="absolute inset-0 z-20 flex items-center justify-center bg-black/90 backdrop-blur-md p-4 hidden">
<div class="glass-panel p-6 md:p-8 rounded-2xl max-w-md w-full text-center border-red-500/40 shadow-red-900/40">
<h2 class="font-orbitron text-3xl md:text-4xl font-black text-red-500 mb-2 glow-text-red">TANQUE DESTRUIDO</h2>
<p class="text-gray-400 text-xs md:text-sm mb-6">¡Tu blindaje ha cedido ante los impactos enemigos!</p>
<div class="bg-gray-900/90 rounded-xl p-4 mb-6 border border-gray-800 space-y-3">
<div class="flex justify-between items-center text-xs md:text-sm">
<span class="text-gray-400 font-orbitron">PUNTUACIÓN TOTAL</span>
<span id="final-score" class="font-orbitron font-bold text-lg md:text-xl text-white">0</span>
</div>
<div class="flex justify-between items-center text-xs md:text-sm">
<span class="text-gray-400 font-orbitron">TANQUES DESTRUIDOS</span>
<span id="final-kills" class="font-orbitron font-bold text-lg md:text-xl text-amber-400">0</span>
</div>
<div class="flex justify-between items-center text-xs md:text-sm">
<span class="text-gray-400 font-orbitron">OLEADAS SURCADAS</span>
<span id="final-wave" class="font-orbitron font-bold text-lg md:text-xl text-cyan-400">0</span>
</div>
</div>
<button id="btn-restart" class="w-full py-3.5 rounded-xl bg-gradient-to-r from-red-600 to-amber-600 hover:from-red-500 hover:to-amber-500 font-orbitron font-black text-base md:text-lg text-white shadow-lg shadow-red-600/30 transition-all transform hover:scale-[1.02] active:scale-95">
REPLEGARSE Y REINTENTAR
</button>
</div>
</div>
<!-- Game Over Screen Overlay -->
<div id="gameover-menu" class="absolute inset-0 z-20 flex items-center justify-center bg-black/90 backdrop-blur-md p-4 hidden">
<div class="glass-panel p-8 md:p-10 rounded-2xl max-w-md w-full text-center border-red-500/40 shadow-red-900/30">
<h2 class="font-orbitron text-4xl font-black text-red-500 mb-2 glow-text-red">TANK DESTROYED</h2>
<p class="text-gray-400 text-sm mb-6">Your armored hull has been compromised!</p>
<div class="bg-gray-900/80 rounded-xl p-4 mb-6 border border-gray-800 space-y-3">
<div class="flex justify-between items-center text-sm">
<span class="text-gray-400 font-orbitron">TOTAL SCORE</span>
<span id="final-score" class="font-orbitron font-bold text-xl text-white">0</span>
</div>
<div class="flex justify-between items-center text-sm">
<span class="text-gray-400 font-orbitron">TANKS DESTROYED</span>
<span id="final-kills" class="font-orbitron font-bold text-xl text-amber-400">0</span>
</div>
<div class="flex justify-between items-center text-sm">
<span class="text-gray-400 font-orbitron">WAVES SURVIVED</span>
<span id="final-wave" class="font-orbitron font-bold text-xl text-cyan-400">0</span>
</div>
</div>
<button id="btn-restart" class="w-full py-4 rounded-xl bg-gradient-to-r from-red-600 to-amber-600 hover:from-red-500 hover:to-amber-500 font-orbitron font-black text-lg text-white shadow-lg shadow-red-600/30 transition-all transform hover:scale-[1.02] active:scale-95">
REDEPLOY TANK
</button>
</div>
</div>
<!-- JavaScript Game Logic -->
<script>
/**
* Tank Warzone 3D - Main Engine & Game Logic
*/
// Global Game State
const state = {
active: false,
score: 0,
kills: 0,
wave: 1,
playerHealth: 100,
maxPlayerHealth: 100,
canShoot: true,
reloadTime: 0.8, // Seconds
reloadProgress: 1,
lastFrameTime: performance.now()
};
// Inputs Setup
const keys = { w: false, a: false, s: false, d: false, space: false };
const mouse = { x: 0, y: 0, raycastPoint: new THREE.Vector3() };
const touchInput = {
left: { active: false, x: 0, y: 0 },
right: { active: false, x: 0, y: 0 }
};
// Three.js Core Components
let scene, camera, renderer, clock;
let playerTank, playerBody, playerTurret, playerBarrel;
const enemies = [];
const projectiles = [];
const particles = [];
const obstacles = [];
const wrecks = [];
const mapBounds = 110;
// Sound Effects (Web Audio API Synthesizer)
class SoundFX {
constructor() {
this.ctx = null;
}
init() {
if (!this.ctx) {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
}
}
playCannon() {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
const now = this.ctx.currentTime;
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(150, now);
osc.frequency.exponentialRampToValueAtTime(30, now + 0.35);
gain.gain.setValueAtTime(0.8, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.35);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + 0.35);
}
playExplosion() {
if (!this.ctx) return;
const now = this.ctx.currentTime;
// Create noise for crunch
const bufferSize = this.ctx.sampleRate * 0.5;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
const noise = this.ctx.createBufferSource();
noise.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(800, now);
filter.frequency.linearRampToValueAtTime(50, now + 0.5);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(1.0, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.5);
noise.connect(filter);
filter.connect(gain);
gain.connect(this.ctx.destination);
noise.start(now);
}
playHit() {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
const now = this.ctx.currentTime;
osc.type = 'square';
osc.frequency.setValueAtTime(300, now);
osc.frequency.exponentialRampToValueAtTime(80, now + 0.15);
gain.gain.setValueAtTime(0.4, now);
gain.gain.exponentialRampToValueAtTime(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();
// Initialize 3D Engine
function initEngine() {
const container = document.getElementById('canvas-container');
// Scene setup with atmospheric fog
scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0e17);
scene.fog = new THREE.FogExp2(0x0a0e17, 0.008);
// Camera setup
camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
// Renderer setup
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);
clock = new THREE.Clock();
// Lighting Setup
const ambientLight = new THREE.AmbientLight(0x99ccff, 0.4);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(60, 100, 40);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
dirLight.shadow.camera.near = 10;
dirLight.shadow.camera.far = 300;
const d = 100;
dirLight.shadow.camera.left = -d;
dirLight.shadow.camera.right = d;
dirLight.shadow.camera.top = d;
dirLight.shadow.camera.bottom = -d;
scene.add(dirLight);
// Grid Ground & Environment
buildEnvironment();
// Create Player Tank
playerTank = createTankModel(0x00f0ff, true);
playerBody = playerTank.getObjectByName("body");
playerTurret = playerTank.getObjectByName("turret");
playerBarrel = playerTank.getObjectByName("barrel");
scene.add(playerTank);
playerTank.position.set(0, 0, 0);
// Window Resize Listener
window.addEventListener('resize', onWindowResize);
// Event Listeners for Controls
setupInputListeners();
}
// Build Environment (Ground Grid, Rocks, Barriers)
function buildEnvironment() {
// Textured Ground Plane
const groundGeo = new THREE.PlaneGeometry(260, 260, 64, 64);
const groundMat = new THREE.MeshStandardMaterial({
color: 0x121824,
roughness: 0.8,
metalness: 0.2,
});
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
// Floor Grid Helper (Sci-fi Aesthetic)
const gridHelper = new THREE.GridHelper(260, 52, 0x00e5ff, 0x1d2c4d);
gridHelper.position.y = 0.05;
scene.add(gridHelper);
// Outer Arena Barriers
const wallMat = new THREE.MeshStandardMaterial({ color: 0x0f172a, roughness: 0.5 });
const borderThickness = 4;
const borderHeight = 6;
const wallGeomH = new THREE.BoxGeometry(260, borderHeight, borderThickness);
const wallGeomV = new THREE.BoxGeometry(borderThickness, borderHeight, 260);
const nWall = new THREE.Mesh(wallGeomH, wallMat);
nWall.position.set(0, borderHeight/2, -130);
scene.add(nWall); obstacles.push(nWall);
const sWall = new THREE.Mesh(wallGeomH, wallMat);
sWall.position.set(0, borderHeight/2, 130);
scene.add(sWall); obstacles.push(sWall);
const eWall = new THREE.Mesh(wallGeomV, wallMat);
eWall.position.set(130, borderHeight/2, 0);
scene.add(eWall); obstacles.push(eWall);
const wWall = new THREE.Mesh(wallGeomV, wallMat);
wWall.position.set(-130, borderHeight/2, 0);
scene.add(wWall); obstacles.push(wWall);
// Scatter Low-Poly Obstacles/Bunkers
const boxGeo = new THREE.BoxGeometry(1, 1, 1);
const obsMat = new THREE.MeshStandardMaterial({ color: 0x26334d, roughness: 0.6 });
const seedPositions = [
{x: -30, z: -30, w: 12, h: 6, d: 12},
{x: 30, z: 30, w: 10, h: 5, d: 14},
{x: -40, z: 40, w: 14, h: 8, d: 8},
{x: 40, z: -40, w: 8, h: 6, d: 16},
{x: 0, z: 60, w: 16, h: 7, d: 10},
{x: 0, z: -60, w: 14, h: 6, d: 14},
{x: -70, z: 0, w: 10, h: 7, d: 20},
{x: 70, z: 0, w: 12, h: 6, d: 18}
];
seedPositions.forEach(p => {
const obs = new THREE.Mesh(boxGeo, obsMat);
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);
});
}
// Generate Detailed 3D Tank Model
function createTankModel(primaryColor, isPlayer = false) {
const tankGroup = new THREE.Group();
const bodyMat = new THREE.MeshStandardMaterial({ color: primaryColor, roughness: 0.4, metalness: 0.5 });
const darkMat = new THREE.MeshStandardMaterial({ color: 0x151d2a, roughness: 0.8 });
const barrelMat = new THREE.MeshStandardMaterial({ color: 0x334155, metalness: 0.8, roughness: 0.3 });
const glowMat = new THREE.MeshBasicMaterial({ color: isPlayer ? 0x00f0ff : 0xff3333 });
// Hull Base
const bodyGeo = new THREE.BoxGeometry(4.2, 1.4, 6);
const body = new THREE.Mesh(bodyGeo, bodyMat);
body.position.y = 1.1;
body.castShadow = true;
body.receiveShadow = true;
body.name = "body";
tankGroup.add(body);
// Tracks Left & Right
const trackGeo = new THREE.BoxGeometry(0.9, 1.2, 6.4);
const trackLeft = new THREE.Mesh(trackGeo, darkMat);
trackLeft.position.set(-2.4, 0.8, 0);
trackLeft.castShadow = true;
tankGroup.add(trackLeft);
const trackRight = new THREE.Mesh(trackGeo, darkMat);
trackRight.position.set(2.4, 0.8, 0);
trackRight.castShadow = true;
tankGroup.add(trackRight);
// Turret Group
const turretGroup = new THREE.Group();
turretGroup.position.set(0, 2.0, -0.2);
turretGroup.name = "turret";
const turretGeo = new THREE.BoxGeometry(2.8, 1.1, 3.4);
const turret = new THREE.Mesh(turretGeo, bodyMat);
turret.position.set(0, 0, 0);
turret.castShadow = true;
turretGroup.add(turret);
// Turret Glow Strip
const visorGeo = new THREE.BoxGeometry(2.0, 0.25, 0.2);
const visor = new THREE.Mesh(visorGeo, glowMat);
visor.position.set(0, 0.2, -1.75);
turretGroup.add(visor);
// Barrel Group
const barrelGroup = new THREE.Group();
barrelGroup.position.set(0, 0, -1.6);
barrelGroup.name = "barrel";
const barrelGeo = new THREE.CylinderGeometry(0.22, 0.25, 4.2, 16);
barrelGeo.rotateX(-Math.PI / 2);
const barrel = new THREE.Mesh(barrelGeo, barrelMat);
barrel.position.set(0, 0, -2.1);
barrel.castShadow = true;
barrelGroup.add(barrel);
// Cannon Muzzle Tip
const tipGeo = new THREE.CylinderGeometry(0.3, 0.3, 0.6, 16);
tipGeo.rotateX(-Math.PI / 2);
const tip = new THREE.Mesh(tipGeo, darkMat);
tip.position.set(0, 0, -4.2);
barrelGroup.add(tip);
turretGroup.add(barrelGroup);
tankGroup.add(turretGroup);
return tankGroup;
}
// Spawn Enemies in Waves
function spawnWave(waveNum) {
const enemyCount = 2 + waveNum * 2;
state.wave = waveNum;
document.getElementById('hud-wave').innerText = state.wave;
for (let i = 0; i < enemyCount; i++) {
// Random position away from player
let px, pz;
do {
px = (Math.random() - 0.5) * 200;
pz = (Math.random() - 0.5) * 200;
} while (Math.hypot(px - playerTank.position.x, pz - playerTank.position.z) < 40);
const enemyMesh = createTankModel(0xe11d48, false);
enemyMesh.position.set(px, 0, pz);
const enemyObj = {
mesh: enemyMesh,
turret: enemyMesh.getObjectByName("turret"),
barrel: enemyMesh.getObjectByName("barrel"),
health: 60 + waveNum * 15,
maxHealth: 60 + waveNum * 15,
reloadTimer: Math.random() * 2,
speed: 8 + Math.min(waveNum * 0.5, 6),
radius: 3.2
};
// Add 3D Health Bar
const hpBarGroup = new THREE.Group();
const hpBgGeo = new THREE.PlaneGeometry(3.5, 0.4);
const hpBgMat = new THREE.MeshBasicMaterial({ color: 0x000000, side: THREE.DoubleSide });
const hpBg = new THREE.Mesh(hpBgGeo, hpBgMat);
const hpFgGeo = new THREE.PlaneGeometry(3.4, 0.3);
const hpFgMat = new THREE.MeshBasicMaterial({ color: 0xff2244, side: THREE.DoubleSide });
const hpFg = new THREE.Mesh(hpFgGeo, hpFgMat);
hpFg.position.z = 0.01;
hpFg.name = "hpFg";
hpBarGroup.add(hpBg);
hpBarGroup.add(hpFg);
hpBarGroup.position.set(0, 4.2, 0);
enemyMesh.add(hpBarGroup);
enemyObj.hpBarGroup = hpBarGroup;
enemyObj.hpFg = hpFg;
enemies.push(enemyObj);
scene.add(enemyMesh);
}
}
// Fire Cannon Shell
function fireShell(owner, originPos, direction, isPlayer = false) {
sfx.playCannon();
// Recoil Animation
const barrel = owner.barrel;
if (barrel) {
barrel.position.z = -1.0;
}
// Shell Mesh
const shellGeo = new THREE.SphereGeometry(0.35, 8, 8);
const shellMat = new THREE.MeshBasicMaterial({
color: isPlayer ? 0x00ffff : 0xff5500
});
const shell = new THREE.Mesh(shellGeo, shellMat);
shell.position.copy(originPos);
// Light flash at muzzle
const flash = new THREE.PointLight(isPlayer ? 0x00ffff : 0xffaa00, 3, 10);
flash.position.copy(originPos);
scene.add(flash);
setTimeout(() => scene.remove(flash), 60);
projectiles.push({
mesh: shell,
velocity: direction.clone().multiplyScalar(isPlayer ? 75 : 55),
isPlayer: isPlayer,
life: 2.5
});
scene.add(shell);
}
// Spawn Explosion Effect & Debris
function createExplosion(position, color = 0xff6600, scale = 1.0) {
sfx.playExplosion();
const particleCount = Math.floor(25 * scale);
for (let i = 0; i < particleCount; i++) {
const pGeo = new THREE.BoxGeometry(0.3 * scale, 0.3 * scale, 0.3 * scale);
const pMat = new THREE.MeshBasicMaterial({
color: Math.random() > 0.5 ? color : 0xffd700
});
const p = new THREE.Mesh(pGeo, pMat);
p.position.copy(position);
const velocity = new THREE.Vector3(
(Math.random() - 0.5) * 20 * scale,
(Math.random() * 15 + 5) * scale,
(Math.random() - 0.5) * 20 * scale
);
particles.push({
mesh: p,
velocity: velocity,
life: 0.6 + Math.random() * 0.4,
maxLife: 1.0
});
scene.add(p);
}
// Flash sphere
const flashGeo = new THREE.SphereGeometry(2 * scale, 16, 16);
const flashMat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.9 });
const flash = new THREE.Mesh(flashGeo, flashMat);
flash.position.copy(position);
scene.add(flash);
let opacity = 0.9;
const fadeInterval = setInterval(() => {
opacity -= 0.15;
if (opacity <= 0) {
scene.remove(flash);
clearInterval(fadeInterval);
} else {
flash.material.opacity = opacity;
}
}, 30);
}
// Spawn Charred Tank Wreckage
function spawnWreckage(position, rotation) {
const wreckMat = new THREE.MeshStandardMaterial({ color: 0x1c1917, roughness: 0.9 });
const wreckGeo = new THREE.BoxGeometry(4.2, 1.2, 5.8);
const wreck = new THREE.Mesh(wreckGeo, wreckMat);
wreck.position.copy(position);
wreck.position.y = 0.6;
wreck.rotation.copy(rotation);
scene.add(wreck);
wrecks.push(wreck);
// Emit persistent smoke for 5 seconds
const smokeInterval = setInterval(() => {
if (Math.random() < 0.6) {
const sGeo = new THREE.SphereGeometry(0.5 + Math.random() * 0.5, 6, 6);
const sMat = new THREE.MeshBasicMaterial({ color: 0x444444, transparent: true, opacity: 0.4 });
const smoke = new THREE.Mesh(sGeo, sMat);
smoke.position.copy(position).add(new THREE.Vector3((Math.random()-0.5)*2, 1, (Math.random()-0.5)*2));
scene.add(smoke);
particles.push({
mesh: smoke,
velocity: new THREE.Vector3(0, 2 + Math.random()*2, 0),
life: 1.2,
maxLife: 1.2,
isSmoke: true
});
}
}, 200);
setTimeout(() => clearInterval(smokeInterval), 6000);
}
// Setup Player Controls / Input Listeners
function setupInputListeners() {
// Keyboard
window.addEventListener('keydown', (e) => {
if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.w = true;
if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.s = true;
if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.a = true;
if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.d = true;
if (e.code === 'Space') {
if (state.active && state.canShoot) triggerPlayerShoot();
}
});
window.addEventListener('keyup', (e) => {
if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.w = false;
if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.s = false;
if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.a = false;
if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.d = false;
});
// Mouse Aiming via Raycasting onto ground plane
const raycaster = new THREE.Raycaster();
const groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
window.addEventListener('mousemove', (e) => {
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
raycaster.ray.intersectPlane(groundPlane, mouse.raycastPoint);
});
window.addEventListener('mousedown', (e) => {
if (e.button === 0 && state.active && state.canShoot) {
sfx.init();
triggerPlayerShoot();
}
});
// Mobile Touch Joystick Handlers
setupTouchJoystick('joystick-left', 'knob-left', (dx, dy) => {
touchInput.left.x = dx;
touchInput.left.y = dy;
});
setupTouchJoystick('joystick-right', 'knob-right', (dx, dy) => {
touchInput.right.x = dx;
touchInput.right.y = dy;
});
document.getElementById('btn-fire').addEventListener('touchstart', (e) => {
e.preventDefault();
sfx.init();
if (state.active && state.canShoot) triggerPlayerShoot();
});
// Menu Buttons
document.getElementById('btn-start').addEventListener('click', () => {
sfx.init();
startGame();
});
document.getElementById('btn-restart').addEventListener('click', () => {
sfx.init();
restartGame();
});
}
// Touch Joystick Helper Logic
function setupTouchJoystick(zoneId, knobId, onChange) {
const zone = document.getElementById(zoneId);
const knob = document.getElementById(knobId);
let active = false;
let touchId = null;
let startX = 0, startY = 0;
zone.addEventListener('touchstart', (e) => {
e.preventDefault();
const touch = e.changedTouches[0];
touchId = touch.identifier;
active = true;
const rect = zone.getBoundingClientRect();
startX = rect.left + rect.width / 2;
startY = rect.top + rect.height / 2;
});
window.addEventListener('touchmove', (e) => {
if (!active) return;
for (let i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
if (touch.identifier === touchId) {
let dx = touch.clientX - startX;
let dy = touch.clientY - startY;
const dist = Math.hypot(dx, dy);
const maxDist = 45;
if (dist > maxDist) {
dx = (dx / dist) * maxDist;
dy = (dy / dist) * maxDist;
}
knob.style.transform = `translate(${dx}px, ${dy}px)`;
onChange(dx / maxDist, dy / maxDist);
}
}
});
const endHandler = (e) => {
if (!active) return;
for (let i = 0; i < e.changedTouches.length; i++) {
if (e.changedTouches[i].identifier === touchId) {
active = false;
knob.style.transform = `translate(0px, 0px)`;
onChange(0, 0);
}
}
};
window.addEventListener('touchend', endHandler);
window.addEventListener('touchcancel', endHandler);
}
// Trigger Player Shot Action
function triggerPlayerShoot() {
if (!state.canShoot || !state.active) return;
state.canShoot = false;
state.reloadProgress = 0;
// Get Barrel tip position
const muzzleWorldPos = new THREE.Vector3();
playerBarrel.getWorldPosition(muzzleWorldPos);
// Get Barrel direction vector
const dir = new THREE.Vector3();
playerTurret.getWorldDirection(dir);
dir.negate(); // Barrel points back along Z in local space
fireShell({ barrel: playerBarrel }, muzzleWorldPos, dir, true);
}
// Start / Reset Game Session
function startGame() {
document.getElementById('start-menu').classList.add('hidden');
document.getElementById('gameover-menu').classList.add('hidden');
document.getElementById('hud-overlay').classList.remove('hidden');
state.active = true;
state.score = 0;
state.kills = 0;
state.playerHealth = 100;
state.wave = 1;
updateHUD();
// Clear old enemies, projectiles, wrecks
enemies.forEach(e => scene.remove(e.mesh));
enemies.length = 0;
projectiles.forEach(p => scene.remove(p.mesh));
projectiles.length = 0;
wrecks.forEach(w => scene.remove(w));
wrecks.length = 0;
playerTank.position.set(0, 0, 0);
playerTank.rotation.set(0, 0, 0);
spawnWave(1);
}
function restartGame() {
startGame();
}
function triggerGameOver() {
state.active = false;
createExplosion(playerTank.position, 0x00f0ff, 2.5);
document.getElementById('hud-overlay').classList.add('hidden');
document.getElementById('gameover-menu').classList.remove('hidden');
document.getElementById('final-score').innerText = state.score;
document.getElementById('final-kills').innerText = state.kills;
document.getElementById('final-wave').innerText = state.wave;
}
function updateHUD() {
const hpPct = Math.max(0, (state.playerHealth / state.maxPlayerHealth) * 100);
document.getElementById('health-bar').style.width = hpPct + '%';
document.getElementById('health-text').innerText = `${Math.ceil(state.playerHealth)} HP`;
document.getElementById('hud-score').innerText = state.score;
document.getElementById('hud-kills').innerText = state.kills;
// Color coding low health
if (hpPct < 30) {
document.getElementById('health-bar').className = "bg-red-600 h-full w-full transition-all duration-200";
} else if (hpPct < 60) {
document.getElementById('health-bar').className = "bg-amber-500 h-full w-full transition-all duration-200";
} else {
document.getElementById('health-bar').className = "bg-gradient-to-r from-emerald-500 to-cyan-400 h-full w-full transition-all duration-200";
}
}
// Render Dynamic Minimap Radar
function updateMinimap() {
const canvas = document.getElementById('minimap-canvas');
const ctx = canvas.getContext('2d');
const w = canvas.width;
const h = canvas.height;
const scale = w / 260; // Map bounds scale ratio
ctx.clearRect(0, 0, w, h);
// Radar background sweep lines
ctx.fillStyle = 'rgba(10, 16, 26, 0.9)';
ctx.fillRect(0, 0, w, h);
ctx.strokeStyle = 'rgba(0, 255, 204, 0.15)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(w/2, h/2, w/3, 0, Math.PI*2);
ctx.stroke();
// Draw Obstacles
ctx.fillStyle = 'rgba(255, 255, 255, 0.15)';
obstacles.forEach(obs => {
const ox = (obs.position.x + 130) * scale;
const oz = (obs.position.z + 130) * scale;
ctx.fillRect(ox - 3, oz - 3, 6, 6);
});
// Draw Enemies (Red Dots)
ctx.fillStyle = '#ff3355';
enemies.forEach(e => {
const ex = (e.mesh.position.x + 130) * scale;
const ez = (e.mesh.position.z + 130) * scale;
ctx.beginPath();
ctx.arc(ex, ez, 3, 0, Math.PI*2);
ctx.fill();
});
// Draw Player (Cyan Icon)
if (state.playerHealth > 0) {
const px = (playerTank.position.x + 130) * scale;
const pz = (playerTank.position.z + 130) * scale;
ctx.fillStyle = '#00f0ff';
ctx.beginPath();
ctx.arc(px, pz, 4, 0, Math.PI*2);
ctx.fill();
}
}
// Window Resize Handler
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
/**
* Main Frame Animation & Update Loop
*/
function animate() {
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.1);
if (state.active) {
updatePlayer(dt);
updateEnemies(dt);
updateProjectiles(dt);
updateParticles(dt);
updateCamera();
updateMinimap();
// Reload Bar Progress
if (!state.canShoot) {
state.reloadProgress += dt / state.reloadTime;
if (state.reloadProgress >= 1) {
state.reloadProgress = 1;
state.canShoot = true;
document.getElementById('reload-text').innerText = "READY";
document.getElementById('reload-text').className = "font-orbitron text-xs text-emerald-400";
} else {
document.getElementById('reload-text').innerText = "RELOADING";
document.getElementById('reload-text').className = "font-orbitron text-xs text-amber-400";
}
document.getElementById('reload-bar').style.width = (state.reloadProgress * 100) + '%';
}
// Check Wave Completion
if (enemies.length === 0) {
state.wave++;
state.playerHealth = Math.min(state.maxPlayerHealth, state.playerHealth + 30); // Repair bonus
updateHUD();
spawnWave(state.wave);
}
} else {
// Menu idle camera rotation
const time = Date.now() * 0.0003;
camera.position.x = Math.sin(time) * 40;
camera.position.z = Math.cos(time) * 40;
camera.position.y = 25;
camera.lookAt(0, 0, 0);
}
renderer.render(scene, camera);
}
// Update Player Tank Physics & Controls
function updatePlayer(dt) {
const moveSpeed = 16.0;
const turnSpeed = 2.4;
// Combine Keyboard & Touch Input for Movement
let moveDir = 0;
let turnDir = 0;
if (keys.w) moveDir += 1;
if (keys.s) moveDir -= 1;
if (keys.a) turnDir += 1;
if (keys.d) turnDir -= 1;
if (touchInput.left.y !== 0) moveDir = -touchInput.left.y;
if (touchInput.left.x !== 0) turnDir = -touchInput.left.x;
// Hull Rotation
playerTank.rotation.y += turnDir * turnSpeed * dt;
// Forward/Backward Motion
if (moveDir !== 0) {
const forward = new THREE.Vector3(0, 0, -1).applyAxisAngle(new THREE.Vector3(0, 1, 0), playerTank.rotation.y);
const nextPos = playerTank.position.clone().add(forward.multiplyScalar(moveDir * moveSpeed * dt));
// Collision Detection with Obstacles & Boundaries
let collide = false;
if (Math.abs(nextPos.x) > mapBounds || Math.abs(nextPos.z) > mapBounds) collide = true;
for (let obs of obstacles) {
if (nextPos.distanceTo(obs.position) < (obs.radius + 2.5)) {
collide = true;
break;
}
}
if (!collide) {
playerTank.position.copy(nextPos);
}
}
// Turret Aiming (Mouse Raycast or Touch Right Joystick)
if (touchInput.right.x !== 0 || touchInput.right.y !== 0) {
// Touch aiming
const targetAngle = Math.atan2(-touchInput.right.x, -touchInput.right.y);
playerTurret.rotation.y = targetAngle - playerTank.rotation.y;
} else {
// Mouse aiming
const targetPoint = mouse.raycastPoint.clone();
const turretWorldPos = new THREE.Vector3();
playerTurret.getWorldPosition(turretWorldPos);
const dx = targetPoint.x - turretWorldPos.x;
const dz = targetPoint.z - turretWorldPos.z;
const targetWorldAngle = Math.atan2(dx, dz) + Math.PI;
playerTurret.rotation.y = targetWorldAngle - playerTank.rotation.y;
}
// Smooth Barrel Recoil Recovery
if (playerBarrel.position.z < 0) {
playerBarrel.position.z = Math.min(0, playerBarrel.position.z + dt * 4.0);
}
}
// Update Enemy Tanks AI Logic
function updateEnemies(dt) {
enemies.forEach((enemy, index) => {
const eMesh = enemy.mesh;
const distToPlayer = eMesh.position.distanceTo(playerTank.position);
// Move towards player if far, stay in firing range
if (distToPlayer > 18 && distToPlayer < 120) {
const dir = new THREE.Vector3().subVectors(playerTank.position, eMesh.position).normalize();
const targetAngle = Math.atan2(dir.x, dir.z) + Math.PI;
// Smooth rotation towards player
let diff = targetAngle - eMesh.rotation.y;
// Normalize angle
diff = Math.atan2(Math.sin(diff), Math.cos(diff));
eMesh.rotation.y += Math.sign(diff) * Math.min(Math.abs(diff), dt * 1.8);
// Move forward
const forward = new THREE.Vector3(0, 0, -1).applyAxisAngle(new THREE.Vector3(0, 1, 0), eMesh.rotation.y);
const nextPos = eMesh.position.clone().add(forward.multiplyScalar(enemy.speed * dt));
// Avoid obstacles & bounds
let collide = false;
if (Math.abs(nextPos.x) > mapBounds || Math.abs(nextPos.z) > mapBounds) collide = true;
for (let obs of obstacles) {
if (nextPos.distanceTo(obs.position) < (obs.radius + enemy.radius)) {
collide = true; break;
}
}
if (!collide) {
eMesh.position.copy(nextPos);
}
}
// Turret auto-aim at player
const eTurretPos = new THREE.Vector3();
enemy.turret.getWorldPosition(eTurretPos);
const aimDir = new THREE.Vector3().subVectors(playerTank.position, eTurretPos);
const aimAngle = Math.atan2(aimDir.x, aimDir.z) + Math.PI;
enemy.turret.rotation.y = aimAngle - eMesh.rotation.y;
// Firing AI
enemy.reloadTimer -= dt;
if (distToPlayer < 80 && enemy.reloadTimer <= 0) {
enemy.reloadTimer = 2.2 + Math.random() * 1.5;
const muzzlePos = new THREE.Vector3();
enemy.barrel.getWorldPosition(muzzlePos);
const fireVector = aimDir.clone().normalize();
fireShell(enemy, muzzlePos, fireVector, false);
}
// Barrel Recoil Recovery
if (enemy.barrel.position.z < 0) {
enemy.barrel.position.z = Math.min(0, enemy.barrel.position.z + dt * 4.0);
}
// Billboard Health Bar to face camera
enemy.hpBarGroup.quaternion.copy(camera.quaternion);
});
}
// Update Projectiles & Hit Detection
function updateProjectiles(dt) {
for (let i = projectiles.length - 1; i >= 0; i--) {
const p = projectiles[i];
p.life -= dt;
const nextPos = p.mesh.position.clone().addScaledVector(p.velocity, dt);
// Collision with Map Obstacles / Walls
let hitObstacle = false;
if (Math.abs(nextPos.x) > mapBounds || Math.abs(nextPos.z) > mapBounds) hitObstacle = true;
if (!hitObstacle) {
for (let obs of obstacles) {
if (nextPos.distanceTo(obs.position) < obs.radius) {
hitObstacle = true;
break;
}
}
}
if (hitObstacle) {
createExplosion(p.mesh.position, 0xffaa00, 0.7);
scene.remove(p.mesh);
projectiles.splice(i, 1);
continue;
}
// Hit Detection: Player Shell hitting Enemy Tank
if (p.isPlayer) {
let hitEnemyIndex = -1;
for (let eIdx = 0; eIdx < enemies.length; eIdx++) {
const enemy = enemies[eIdx];
if (nextPos.distanceTo(enemy.mesh.position) < enemy.radius) {
hitEnemyIndex = eIdx;
break;
}
}
if (hitEnemyIndex !== -1) {
const enemy = enemies[hitEnemyIndex];
sfx.playHit();
enemy.health -= 35;
createExplosion(nextPos, 0xffaa00, 0.9);
// Update Enemy 3D HP bar scale
const hpPct = Math.max(0, enemy.health / enemy.maxHealth);
enemy.hpFg.scale.set(hpPct, 1, 1);
// Check Enemy Destruction
if (enemy.health <= 0) {
createExplosion(enemy.mesh.position, 0xff3300, 1.8);
spawnWreckage(enemy.mesh.position, enemy.mesh.rotation);
scene.remove(enemy.mesh);
enemies.splice(hitEnemyIndex, 1);
state.kills++;
state.score += 150;
updateHUD();
}
scene.remove(p.mesh);
projectiles.splice(i, 1);
continue;
}
}
// Hit Detection: Enemy Shell hitting Player Tank
else {
if (nextPos.distanceTo(playerTank.position) < 2.8) {
sfx.playHit();
state.playerHealth -= 15;
createExplosion(nextPos, 0xff0033, 1.2);
updateHUD();
if (state.playerHealth <= 0) {
triggerGameOver();
}
scene.remove(p.mesh);
projectiles.splice(i, 1);
continue;
}
}
// Lifespan expiration
if (p.life <= 0) {
scene.remove(p.mesh);
projectiles.splice(i, 1);
} else {
p.mesh.position.copy(nextPos);
}
}
}
// Update Explosion Particle Physics
function updateParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.life -= dt;
p.mesh.position.addScaledVector(p.velocity, dt);
if (!p.isSmoke) {
p.velocity.y -= 25 * dt; // Gravity effect
}
const opacity = p.life / p.maxLife;
p.mesh.scale.setScalar(opacity);
if (p.life <= 0) {
scene.remove(p.mesh);
particles.splice(i, 1);
}
}
}
// Smooth Camera Follow Logic
function updateCamera() {
const cameraOffset = new THREE.Vector3(0, 22, 26); // Third-person height and distance
const targetCamPos = playerTank.position.clone().add(cameraOffset);
camera.position.lerp(targetCamPos, 0.1);
camera.lookAt(playerTank.position.x, playerTank.position.y + 1.5, playerTank.position.z);
}
// Bootstrapping Engine on Page Load
window.onload = () => {
initEngine();
animate();
};
</script>
</body>
</html>

No hay comentarios:
Publicar un comentario