<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Batalla Naval Pirata 3D</title>
<!-- Tailwind CSS para el diseño de la UI -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Three.js para la representación 3D -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<!-- FontAwesome para iconos -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
body {
margin: 0;
overflow: hidden;
font-family: 'Inter', sans-serif;
user-select: none;
background-color: #0f172a;
}
#canvas-container {
width: 100vw;
height: 100vh;
display: block;
}
/* Estilos personalizados para los Sliders */
input[type=range] {
accent-color: #f59e0b;
}
.glass-panel {
background: rgba(15, 23, 42, 0.75);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.15);
}
</style>
</head>
<body class="relative">
<!-- Canvas principal para Three.js -->
<div id="canvas-container"></div>
<!-- HUD / Interfaz de Usuario de Puntuación y Estado -->
<div class="absolute top-4 left-4 flex flex-col gap-3 pointer-events-none z-10">
<div class="glass-panel text-white p-4 rounded-xl shadow-2xl flex items-center gap-4">
<div class="bg-amber-500/20 p-3 rounded-lg border border-amber-500/30 text-amber-400">
<i class="fa-solid me-1 fa-skull-crossbones text-3xl"></i>
</div>
<div>
<p class="text-xs uppercase tracking-wider text-slate-400 font-semibold">Barcos Hundidos</p>
<h1 id="score-text" class="text-3xl font-black text-amber-400">0</h1>
</div>
</div>
<div class="glass-panel text-white p-3 rounded-xl shadow-lg flex items-center justify-between min-w-[200px]">
<span class="text-xs text-slate-300 font-medium"><i class="fa-solid fa-ship me-2 text-cyan-400"></i>Enemigos activos:</span>
<span id="enemy-count" class="font-bold text-cyan-400 text-lg">0</span>
</div>
</div>
<!-- Panel de Calibración de Velocidades (Sliders) -->
<div class="absolute top-4 right-4 glass-panel text-white p-5 rounded-2xl shadow-2xl w-80 z-10 border border-slate-700">
<div class="flex items-center justify-between border-b border-slate-700/80 pb-3 mb-4">
<h2 class="font-bold text-amber-400 flex items-center gap-2">
<i class="fa-solid fa-sliders text-amber-500"></i> Calibrador Naval
</h2>
<button id="btn-pause" class="bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs px-2.5 py-1 rounded-md transition border border-slate-600">
<i class="fa-solid fa-pause me-1"></i> Pausa
</button>
</div>
<!-- Slider Velocidad del Jugador -->
<div class="mb-4">
<div class="flex justify-between text-xs font-semibold mb-1">
<label for="player-speed-slider" class="text-emerald-400 flex items-center gap-1">
<i class="fa-solid fa-gauge-high"></i> Tu Velocidad:
</label>
<span id="player-speed-val" class="text-emerald-400 font-mono">1.0x</span>
</div>
<input type="range" id="player-speed-slider" min="0.2" max="3.0" step="0.1" value="1.0" class="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer">
<div class="flex justify-between text-[10px] text-slate-400 mt-1">
<span>Lento</span>
<span>Normal</span>
<span>Súper Rápido</span>
</div>
</div>
<!-- Slider Velocidad Enemigos -->
<div>
<div class="flex justify-between text-xs font-semibold mb-1">
<label for="enemy-speed-slider" class="text-rose-400 flex items-center gap-1">
<i class="fa-solid fa-turtle"></i> Velocidad Enemigos:
</label>
<span id="enemy-speed-val" class="text-rose-400 font-mono">0.2x</span>
</div>
<input type="range" id="enemy-speed-slider" min="0.02" max="0.8" step="0.02" value="0.2" class="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer">
<div class="flex justify-between text-[10px] text-slate-400 mt-1">
<span>Casi Estáticos</span>
<span>Muy Lento</span>
<span>Moderado</span>
</div>
</div>
</div>
<!-- Guía de Controles Móviles / Escritorio (Inferior) -->
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 glass-panel text-slate-300 px-6 py-3 rounded-full text-xs flex items-center gap-6 shadow-xl z-10 border border-slate-700/60">
<div class="flex items-center gap-2">
<span class="bg-slate-800 text-amber-400 px-2 py-0.5 rounded font-mono border border-slate-600">W / S</span> o <span class="bg-slate-800 text-amber-400 px-2 py-0.5 rounded font-mono border border-slate-600">↑ / ↓</span>
<span>Avanzar / Atrás</span>
</div>
<div class="h-4 w-px bg-slate-700"></div>
<div class="flex items-center gap-2">
<span class="bg-slate-800 text-amber-400 px-2 py-0.5 rounded font-mono border border-slate-600">A / D</span> o <span class="bg-slate-800 text-amber-400 px-2 py-0.5 rounded font-mono border border-slate-600">← / →</span>
<span>Girar</span>
</div>
<div class="h-4 w-px bg-slate-700"></div>
<div class="flex items-center gap-2">
<span class="bg-slate-800 text-amber-400 px-3 py-0.5 rounded font-mono border border-slate-600">ESPACIO</span> o <span class="bg-slate-800 text-amber-400 px-2 py-0.5 rounded font-mono border border-slate-600">CLIC</span>
<span class="text-amber-400 font-bold">¡FUEGO! (Cañones)</span>
</div>
</div>
<!-- Controles Táctiles para Móviles -->
<div class="md:hidden absolute bottom-20 left-4 z-20 flex flex-col gap-2">
<div class="flex justify-center">
<button id="btn-up" class="w-12 h-12 glass-panel active:bg-amber-500/40 rounded-full text-white text-xl flex items-center justify-center border border-slate-600"><i class="fa-solid fa-arrow-up"></i></button>
</div>
<div class="flex gap-4">
<button id="btn-left" class="w-12 h-12 glass-panel active:bg-amber-500/40 rounded-full text-white text-xl flex items-center justify-center border border-slate-600"><i class="fa-solid fa-arrow-left"></i></button>
<button id="btn-down" class="w-12 h-12 glass-panel active:bg-amber-500/40 rounded-full text-white text-xl flex items-center justify-center border border-slate-600"><i class="fa-solid fa-arrow-down"></i></button>
<button id="btn-right" class="w-12 h-12 glass-panel active:bg-amber-500/40 rounded-full text-white text-xl flex items-center justify-center border border-slate-600"><i class="fa-solid fa-arrow-right"></i></button>
</div>
</div>
<div class="md:hidden absolute bottom-20 right-4 z-20">
<button id="btn-fire" class="w-20 h-20 bg-gradient-to-tr from-amber-600 to-red-600 active:scale-95 rounded-full text-white font-bold text-sm shadow-2xl flex flex-col items-center justify-center border-2 border-amber-300">
<i class="fa-solid fa-bomb text-xl mb-1"></i>
FUEGO
</button>
</div>
<!-- LÓGICA COMPLETA DEL JUEGO CON THREE.JS -->
<script>
// --- CONFIGURACIÓN E INICIALIZACIÓN ---
let scene, camera, renderer;
let playerBoat, waterMesh;
let cannonballs = [];
let enemies = [];
let particles = [];
let score = 0;
let isPaused = false;
// Factores de calibración (Modificados por Sliders)
let playerSpeedMultiplier = 1.0;
let enemySpeedMultiplier = 0.2;
// Estado de teclas
const keys = {
forward: false,
backward: false,
left: false,
right: false
};
// Configuración del jugador
const playerStats = {
speed: 0,
maxSpeed: 0.45,
acceleration: 0.015,
friction: 0.96,
turnSpeed: 0.035,
cooldown: 0,
cooldownMax: 22 // Cuadros entre disparos
};
const ENEMY_COUNT = 7;
const MAP_SIZE = 400;
// Inicializar el escenario cuando cargue la ventana
window.onload = function () {
initScene();
createWater();
createLights();
playerBoat = createBoat(0x2563eb, true); // Barco azul para el jugador
scene.add(playerBoat);
// Generar barcos enemigos iniciales
for (let i = 0; i < ENEMY_COUNT; i++) {
spawnEnemy();
}
setupControls();
setupUI();
// Iniciar bucle de animación
animate();
};
// --- CONSTRUCCIÓN 3D DE OBJETOS (Three.js Primitivas) ---
function initScene() {
const container = document.getElementById('canvas-container');
// Escena
scene = new THREE.Scene();
scene.background = new THREE.Color(0x7dd3fc); // Cielo azul claro
scene.fog = new THREE.FogExp2(0x7dd3fc, 0.0035);
// Cámara en tercera persona / vista aérea isométrica suave
camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 35, 45);
// Renderizador
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);
// Redimensionamiento de ventana
window.addEventListener('resize', onWindowResize, false);
}
function createLights() {
// Luz Ambiental
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
// Luz Sol (Direccional con sombras)
const sunLight = new THREE.DirectionalLight(0xfffbeb, 0.9);
sunLight.position.set(100, 150, 50);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 2048;
sunLight.shadow.mapSize.height = 2048;
sunLight.shadow.camera.near = 0.5;
sunLight.shadow.camera.far = 500;
const d = 150;
sunLight.shadow.camera.left = -d;
sunLight.shadow.camera.right = d;
sunLight.shadow.camera.top = d;
sunLight.shadow.camera.bottom = -d;
scene.add(sunLight);
}
// Creación del Mar
function createWater() {
const waterGeometry = new THREE.PlaneGeometry(MAP_SIZE * 2, MAP_SIZE * 2, 64, 64);
const waterMaterial = new THREE.MeshStandardMaterial({
color: 0x0284c7,
roughness: 0.1,
metalness: 0.1,
flatShading: true
});
waterMesh = new THREE.Mesh(waterGeometry, waterMaterial);
waterMesh.rotation.x = -Math.PI / 2;
waterMesh.receiveShadow = true;
scene.add(waterMesh);
}
// Constructor de Barco (Pirata o Enemigo)
function createBoat(hullColorHex, isPlayer = false) {
const boatGroup = new THREE.Group();
// Materiales
const hullMat = new THREE.MeshStandardMaterial({ color: hullColorHex, roughness: 0.6 });
const deckMat = new THREE.MeshStandardMaterial({ color: 0x78350f, roughness: 0.8 }); // Madera
const sailMat = new THREE.MeshStandardMaterial({ color: isPlayer ? 0xf8fafc : 0xef4444, roughness: 0.5, side: THREE.DoubleSide });
const woodMat = new THREE.MeshStandardMaterial({ color: 0x451a03 });
const ironMat = new THREE.MeshStandardMaterial({ color: 0x1e293b, metalness: 0.8 });
// 1. Casco del barco (Forma básica con bloque estilizado)
const hullGeo = new THREE.BoxGeometry(3.2, 1.6, 7);
const hull = new THREE.Mesh(hullGeo, hullMat);
hull.position.y = 0.6;
hull.castShadow = true;
hull.receiveShadow = true;
boatGroup.add(hull);
// Proa puntiaguda
const bowGeo = new THREE.ConeGeometry(1.6, 2.8, 4);
const bow = new THREE.Mesh(bowGeo, hullMat);
bow.rotation.x = Math.PI / 2;
bow.rotation.y = Math.PI / 4;
bow.position.set(0, 0.6, 4.4);
bow.scale.set(1, 1, 0.8);
bow.castShadow = true;
boatGroup.add(bow);
// Cubierta
const deckGeo = new THREE.BoxGeometry(3.0, 0.2, 6.6);
const deck = new THREE.Mesh(deckGeo, deckMat);
deck.position.y = 1.45;
boatGroup.add(deck);
// Mástiles y Velas
const mastPositions = isPlayer ? [0.8, -1.2] : [0];
mastPositions.forEach(posZ => {
// Mástil
const mastGeo = new THREE.CylinderGeometry(0.12, 0.15, 6.5);
const mast = new THREE.Mesh(mastGeo, woodMat);
mast.position.set(0, 4.5, posZ);
mast.castShadow = true;
boatGroup.add(mast);
// Velas (Curvadas ligeramente)
const sailGeo = new THREE.PlaneGeometry(3.8, 3.2, 4, 4);
// Deformar la geometría para dar efecto de viento a la vela
const pos = sailGeo.attributes.position;
for (let i = 0; i < pos.count; i++) {
const z = pos.getZ(i);
pos.setZ(i, z + Math.sin(pos.getY(i)) * 0.4);
}
sailGeo.computeVertexNormals();
const sail = new THREE.Mesh(sailGeo, sailMat);
sail.position.set(0, 5, posZ + 0.1);
sail.castShadow = true;
boatGroup.add(sail);
});
// Cañones en los laterales
const cannonGeo = new THREE.CylinderGeometry(0.15, 0.2, 1.2);
cannonGeo.rotateX(Math.PI / 2);
[-1.6, 1.6].forEach(sideX => {
[-1, 1].forEach(posZ => {
const cannon = new THREE.Mesh(cannonGeo, ironMat);
cannon.position.set(sideX, 1.3, posZ);
cannon.rotation.y = sideX > 0 ? Math.PI / 2 : -Math.PI / 2;
cannon.castShadow = true;
boatGroup.add(cannon);
});
});
// Bandera superior
const flagGeo = new THREE.PlaneGeometry(0.8, 0.5);
const flagMat = new THREE.MeshBasicMaterial({ color: isPlayer ? 0xf59e0b : 0x111827, side: THREE.DoubleSide });
const flag = new THREE.Mesh(flagGeo, flagMat);
flag.position.set(0, 7.5, mastPositions[0]);
boatGroup.add(flag);
return boatGroup;
}
// Spawn de Barcos Enemigos en los márgenes
function spawnEnemy() {
const enemyMesh = createBoat(0x991b1b, false); // Rojo oscuro
// Posición aleatoria alrededor del jugador a cierta distancia
const angle = Math.random() * Math.PI * 2;
const distance = 80 + Math.random() * 60;
enemyMesh.position.x = playerBoat ? playerBoat.position.x + Math.cos(angle) * distance : Math.cos(angle) * distance;
enemyMesh.position.z = playerBoat ? playerBoat.position.z + Math.sin(angle) * distance : Math.sin(angle) * distance;
enemyMesh.rotation.y = Math.atan2(playerBoat ? playerBoat.position.x - enemyMesh.position.x : 0, playerBoat ? playerBoat.position.z - enemyMesh.position.z : 0);
// Estado del enemigo
const enemyData = {
mesh: enemyMesh,
isSinking: false,
sinkTimer: 0,
health: 1
};
scene.add(enemyMesh);
enemies.push(enemyData);
document.getElementById('enemy-count').innerText = enemies.filter(e => !e.isSinking).length;
}
// --- SISTEMA DE DISPARO Y BALAS DE CAÑÓN ---
function firePlayerCannons() {
if (playerStats.cooldown > 0 || isPaused) return;
playerStats.cooldown = playerStats.cooldownMax;
// Disparar en ráfaga lateral izquierda y derecha + frontal ligero
const sides = [-1, 1]; // Izquierda y Derecha
sides.forEach(side => {
[-1, 1].forEach(offsetZ => {
createCannonball(side, offsetZ);
});
});
}
function createCannonball(sideDirection, offsetZ) {
const ballGeo = new THREE.SphereGeometry(0.35, 12, 12);
const ballMat = new THREE.MeshStandardMaterial({ color: 0x171717, metalness: 0.9, roughness: 0.2 });
const ball = new THREE.Mesh(ballGeo, ballMat);
ball.castShadow = true;
// Posición inicial (Lado del barco)
const spawnOffset = new THREE.Vector3(sideDirection * 1.8, 1.3, offsetZ);
spawnOffset.applyAxisAngle(new THREE.Vector3(0, 1, 0), playerBoat.rotation.y);
ball.position.copy(playerBoat.position).add(spawnOffset);
// Dirección del proyectil (Perpendicular al barco + ligero impulso)
const angle = playerBoat.rotation.y + (sideDirection * Math.PI / 2);
const speed = 1.3;
const velocity = new THREE.Vector3(
Math.sin(angle) * speed,
0.25, // Elevación para trayectoria parabólica
Math.cos(angle) * speed
);
scene.add(ball);
cannonballs.push({
mesh: ball,
velocity: velocity,
life: 60 // Cuadros antes de caer al agua
});
// Pequeño humo en el cañón al disparar
createExplosionParticles(ball.position, 0xf59e0b, 4, 0.2);
}
// --- EFECTOS DE PARTICULAS (EXPLOSIÓN Y SALPICADURAS) ---
function createExplosionParticles(position, colorHex, count = 15, scale = 0.5) {
for (let i = 0; i < count; i++) {
const pGeo = new THREE.SphereGeometry((Math.random() * 0.3 + 0.1) * scale, 6, 6);
const pMat = new THREE.MeshBasicMaterial({ color: colorHex, transparent: true, opacity: 0.9 });
const p = new THREE.Mesh(pGeo, pMat);
p.position.copy(position);
const vel = new THREE.Vector3(
(Math.random() - 0.5) * 0.6,
Math.random() * 0.5 + 0.1,
(Math.random() - 0.5) * 0.6
);
scene.add(p);
particles.push({
mesh: p,
velocity: vel,
life: 25 + Math.random() * 15,
maxLife: 40
});
}
}
// --- CONTROLES E INTERFAZ (UI) ---
function setupControls() {
// Teclado
window.addEventListener('keydown', (e) => {
if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.forward = true;
if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.backward = true;
if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.left = true;
if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.right = true;
if (e.code === 'Space') firePlayerCannons();
});
window.addEventListener('keyup', (e) => {
if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.forward = false;
if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.backward = false;
if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.left = false;
if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.right = false;
});
// Clic de ratón para disparar
window.addEventListener('pointerdown', (e) => {
// Evitar disparar si se hace clic en elementos UI
if (!e.target.closest('.glass-panel') && !e.target.closest('button') && !e.target.closest('input')) {
firePlayerCannons();
}
});
// Botones táctiles móviles
bindTouchBtn('btn-up', 'forward');
bindTouchBtn('btn-down', 'backward');
bindTouchBtn('btn-left', 'left');
bindTouchBtn('btn-right', 'right');
document.getElementById('btn-fire').addEventListener('click', firePlayerCannons);
}
function bindTouchBtn(id, keyProp) {
const btn = document.getElementById(id);
btn.addEventListener('touchstart', (e) => { e.preventDefault(); keys[keyProp] = true; });
btn.addEventListener('touchend', (e) => { e.preventDefault(); keys[keyProp] = false; });
btn.addEventListener('mousedown', () => { keys[keyProp] = true; });
btn.addEventListener('mouseup', () => { keys[keyProp] = false; });
}
function setupUI() {
// Sliders para calibración
const playerSlider = document.getElementById('player-speed-slider');
const playerValLabel = document.getElementById('player-speed-val');
playerSlider.addEventListener('input', (e) => {
playerSpeedMultiplier = parseFloat(e.target.value);
playerValLabel.innerText = playerSpeedMultiplier.toFixed(1) + 'x';
});
const enemySlider = document.getElementById('enemy-speed-slider');
const enemyValLabel = document.getElementById('enemy-speed-val');
enemySlider.addEventListener('input', (e) => {
enemySpeedMultiplier = parseFloat(e.target.value);
enemyValLabel.innerText = enemySpeedMultiplier.toFixed(2) + 'x';
});
// Botón Pausa
const btnPause = document.getElementById('btn-pause');
btnPause.addEventListener('click', () => {
isPaused = !isPaused;
btnPause.innerHTML = isPaused ? '<i class="fa-solid fa-play me-1"></i> Reanudar' : '<i class="fa-solid fa-pause me-1"></i> Pausa';
});
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// --- BUCLE PRINCIPAL DE JUEGO (ANIMACIÓN Y LÓGICA) ---
function animate() {
requestAnimationFrame(animate);
if (!isPaused) {
updatePlayer();
updateEnemies();
updateCannonballs();
updateParticles();
updateCamera();
animateWater();
}
renderer.render(scene, camera);
}
// Movimiento suave del mar
function animateWater() {
const time = Date.now() * 0.0015;
const pos = waterMesh.geometry.attributes.position;
for (let i = 0; i < pos.count; i++) {
const u = pos.getX(i);
const v = pos.getY(i);
const z = Math.sin(u * 0.1 + time) * 0.3 + Math.cos(v * 0.1 + time) * 0.3;
pos.setZ(i, z);
}
waterMesh.geometry.computeVertexNormals();
waterMesh.geometry.attributes.position.needsUpdate = true;
}
// Lógica de movimiento del Jugador
function updatePlayer() {
if (playerStats.cooldown > 0) playerStats.cooldown--;
// Aceleración y Frenado según Sliders
const effectiveMaxSpeed = playerStats.maxSpeed * playerSpeedMultiplier;
if (keys.forward) {
playerStats.speed = Math.min(playerStats.speed + playerStats.acceleration, effectiveMaxSpeed);
} else if (keys.backward) {
playerStats.speed = Math.max(playerStats.speed - playerStats.acceleration, -effectiveMaxSpeed * 0.4);
} else {
playerStats.speed *= playerStats.friction;
}
// Giro
if (Math.abs(playerStats.speed) > 0.01) {
const dir = playerStats.speed > 0 ? 1 : -1;
if (keys.left) playerBoat.rotation.y += playerStats.turnSpeed * dir;
if (keys.right) playerBoat.rotation.y -= playerStats.turnSpeed * dir;
}
// Desplazamiento
playerBoat.position.x += Math.sin(playerBoat.rotation.y) * playerStats.speed;
playerBoat.position.z += Math.cos(playerBoat.rotation.y) * playerStats.speed;
// Balanceo simulado del barco por el oleaje
const time = Date.now() * 0.003;
playerBoat.rotation.z = Math.sin(time) * 0.04;
playerBoat.rotation.x = Math.cos(time * 0.7) * 0.03;
playerBoat.position.y = Math.sin(time * 1.5) * 0.15;
}
// Lógica Barcos Enemigos (Navegación MUY LENTA hacia el jugador)
function updateEnemies() {
const time = Date.now() * 0.002;
enemies.forEach((enemy, index) => {
if (enemy.isSinking) {
// Animación de hundimiento lenta (se inclina y desciende)
enemy.sinkTimer += 0.015;
enemy.mesh.position.y -= 0.04;
enemy.mesh.rotation.z += 0.01;
enemy.mesh.rotation.x += 0.015;
if (enemy.mesh.position.y < -6) {
// Eliminar completamente y reaparecer otro enemigo
scene.remove(enemy.mesh);
enemies.splice(index, 1);
spawnEnemy();
}
return;
}
// Balanceo normal
enemy.mesh.position.y = Math.sin(time + index) * 0.1;
// Rotación hacia el jugador (Persecución Lenta)
const dx = playerBoat.position.x - enemy.mesh.position.x;
const dz = playerBoat.position.z - enemy.mesh.position.z;
const targetAngle = Math.atan2(dx, dz);
// Suavizar giro del enemigo
let diff = targetAngle - enemy.mesh.rotation.y;
while (diff < -Math.PI) diff += Math.PI * 2;
while (diff > Math.PI) diff -= Math.PI * 2;
enemy.mesh.rotation.y += diff * 0.02;
// Movimiento MUY LENTO hacia adelante regulado por el slider de velocidad enemigo
const baseEnemySpeed = 0.08;
const currentEnemySpeed = baseEnemySpeed * enemySpeedMultiplier;
enemy.mesh.position.x += Math.sin(enemy.mesh.rotation.y) * currentEnemySpeed;
enemy.mesh.position.z += Math.cos(enemy.mesh.rotation.y) * currentEnemySpeed;
});
}
// Lógica Proyectiles de Cañón y Colisiones
function updateCannonballs() {
for (let i = cannonballs.length - 1; i >= 0; i--) {
const b = cannonballs[i];
// Mover bala
b.mesh.position.add(b.velocity);
b.velocity.y -= 0.008; // Gravedad suave
b.life--;
// Verificar colisión con agua o barcos enemigos
let hit = false;
// Colisión con enemigos
enemies.forEach(enemy => {
if (!enemy.isSinking && !hit) {
const dist = b.mesh.position.distanceTo(enemy.mesh.position);
if (dist < 4.2) { // Radio de colisión del barco
hit = true;
enemy.isSinking = true;
// Efectos visuales de explosión y humo
createExplosionParticles(b.mesh.position, 0xf97316, 25, 1.2);
createExplosionParticles(b.mesh.position, 0xef4444, 20, 0.8);
createExplosionParticles(b.mesh.position, 0x334155, 15, 1.5);
// Actualizar Puntuación
score++;
document.getElementById('score-text').innerText = score;
document.getElementById('enemy-count').innerText = enemies.filter(e => !e.isSinking).length;
}
}
});
// Colisión con el agua o límite de vida de la bala
if (!hit && (b.mesh.position.y <= 0 || b.life <= 0)) {
hit = true;
// Salpicadura de agua
createExplosionParticles(new THREE.Vector3(b.mesh.position.x, 0, b.mesh.position.z), 0xe0f2fe, 12, 0.6);
}
if (hit) {
scene.remove(b.mesh);
cannonballs.splice(i, 1);
}
}
}
// Actualizar partículas
function updateParticles() {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.mesh.position.add(p.velocity);
p.life--;
// Desvanecer opacidad
p.mesh.material.opacity = p.life / p.maxLife;
if (p.life <= 0) {
scene.remove(p.mesh);
particles.splice(i, 1);
}
}
}
// Cámara inteligente que sigue al barco del jugador
function updateCamera() {
const cameraOffset = new THREE.Vector3(0, 30, -40); // Posición relativa detrás y arriba
cameraOffset.applyAxisAngle(new THREE.Vector3(0, 1, 0), playerBoat.rotation.y);
const targetCamPos = playerBoat.position.clone().add(cameraOffset);
// Suavizado de movimiento de cámara (Lerp)
camera.position.lerp(targetCamPos, 0.06);
camera.lookAt(playerBoat.position.clone().add(new THREE.Vector3(0, 2, 0)));
}
</script>
</body>
</html>
