https://sites.google.com/view/juego-3d-monigote-en-pasillos-/full-page-embed
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Monigote 3D - Pasillos, Habitaciones y Puertas Interactivas</title>
<!-- Tailwind CSS -->
<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, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
font-family: 'Inter', sans-serif;
background-color: #050505;
}
#webgl-container {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
</style>
</head>
<body class="select-none">
<!-- Canvas para Three.js -->
<div id="webgl-container"></div>
<!-- UI Overlay / Panel de Control -->
<div class="absolute top-4 left-4 z-10 bg-slate-900/80 backdrop-blur-md text-white p-4 rounded-xl border border-slate-700/50 shadow-2xl max-w-xs w-full">
<h1 class="text-xl font-bold mb-2 flex items-center justify-between text-amber-400">
<span class="flex items-center gap-2">🚶 Monigote 3D</span>
<span id="scoreBadge" class="text-xs bg-emerald-500/20 text-emerald-400 border border-emerald-500/40 px-2 py-0.5 rounded-full font-mono font-bold">0 / 10</span>
</h1>
<p class="text-xs text-slate-300 mb-3 leading-relaxed">
Usa WASD para moverte. Presiona <kbd class="px-1 py-0.5 bg-slate-700 rounded text-amber-300 font-mono">ESPACIO</kbd> o <kbd class="px-1 py-0.5 bg-slate-700 rounded text-amber-300 font-mono">CLIC</kbd> para lanzar bolas verdes desde la cabeza.
</p>
<!-- Controles de Teclado -->
<div class="mb-3 bg-slate-800/60 p-2.5 rounded-lg border border-slate-700/40">
<div class="flex justify-between items-center mb-1.5">
<span class="text-xs font-semibold text-slate-400 uppercase tracking-wider block">Controles</span>
<span class="text-[10px] text-amber-400 font-mono">Espacio / Clic: Disparar</span>
</div>
<div class="grid grid-cols-3 gap-1 w-28 mx-auto text-center font-mono font-bold text-sm">
<div></div>
<div class="bg-slate-700 rounded py-1 border border-slate-600 shadow-sm" id="key-w">W</div>
<div></div>
<div class="bg-slate-700 rounded py-1 border border-slate-600 shadow-sm" id="key-a">A</div>
<div class="bg-slate-700 rounded py-1 border border-slate-600 shadow-sm" id="key-s">S</div>
<div class="bg-slate-700 rounded py-1 border border-slate-600 shadow-sm" id="key-d">D</div>
</div>
</div>
<!-- Sliders de Velocidad -->
<div class="space-y-2">
<div>
<div class="flex justify-between items-center text-xs mb-0.5">
<label for="speedSlider" class="font-medium text-slate-300">Velocidad Jugador</label>
<span id="speedValue" class="font-mono text-amber-400 font-bold">5.0</span>
</div>
<input type="range" id="speedSlider" min="1" max="15" step="0.5" value="5"
class="w-full h-1.5 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-amber-400">
</div>
<div>
<div class="flex justify-between items-center text-xs mb-0.5">
<label for="enemySpeedSlider" class="font-medium text-slate-300">Velocidad Enemigos (Rojos)</label>
<span id="enemySpeedValue" class="font-mono text-rose-400 font-bold">3.0</span>
</div>
<input type="range" id="enemySpeedSlider" min="0.5" max="10" step="0.5" value="3"
class="w-full h-1.5 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-rose-500">
</div>
</div>
<!-- Indicador de Puertas -->
<div id="doorAlert" class="mt-2 py-1 text-center bg-amber-500/20 border border-amber-500/50 text-amber-300 rounded-lg text-xs font-bold transition-opacity duration-200 opacity-0">
🚪 ¡PUERTA ABRIÉNDOSE!
</div>
</div>
<!-- Modal de Victoria -->
<div id="victoryModal" class="hidden absolute inset-0 z-50 bg-slate-950/80 backdrop-blur-lg flex items-center justify-center p-4">
<div class="bg-slate-900 border border-emerald-500/40 p-8 rounded-2xl max-w-sm w-full text-center shadow-2xl space-y-4 transform transition-all scale-100">
<div class="text-6xl animate-bounce">🏆</div>
<h2 class="text-3xl font-black text-emerald-400 tracking-wide">¡HAS GANADO!</h2>
<p class="text-slate-300 text-sm leading-relaxed">
Has convertido a los 10 enemigos rojos con tus proyectiles verdes y limpiado el laberinto.
</p>
<button id="restartBtn" class="w-full py-3 bg-gradient-to-r from-emerald-500 to-teal-600 hover:from-emerald-400 hover:to-teal-500 text-slate-950 font-black tracking-wider uppercase rounded-xl shadow-lg transition-transform active:scale-95">
Jugar de Nuevo
</button>
</div>
</div>
<!-- Script del Juego -->
<script>
// Variables principales Three.js
let scene, camera, renderer;
let monigoteGroup, limbs = {};
let spotlight, streetlightGroup;
// Arreglos de colisiones, puertas, enemigos y proyectiles
const wallColliders = [];
const doors = [];
const enemies = [];
const projectiles = [];
// Estado del juego y puntuación
let score = 0;
const TOTAL_ENEMIES = 10;
let globalEnemySpeed = 3.0;
let gameWon = false;
// Estado del personaje
const characterState = {
position: new THREE.Vector3(0, 0, 8),
rotation: 0,
speed: 5.0,
walkCycle: 0,
isMoving: false,
radius: 0.35 // Radio físico del personaje para colisiones
};
// Teclas de movimiento
const keys = { w: false, a: false, s: false, d: false, space: false };
window.onload = function() {
initScene();
buildMap();
createStreetlight();
createMonigote();
spawnEnemies(TOTAL_ENEMIES);
setupControls();
onWindowResize();
animate(0);
};
function initScene() {
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x0a0c16, 0.025);
camera = new THREE.PerspectiveCamera(60, 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;
document.getElementById('webgl-container').appendChild(renderer.domElement);
// Luz ambiental para mantener visibilidad completa del mapa
const ambientLight = new THREE.AmbientLight(0x334155, 1.2);
scene.add(ambientLight);
// Luz direccional de techo ligera
const dirLight = new THREE.DirectionalLight(0x94a3b8, 0.6);
dirLight.position.set(10, 20, 10);
scene.add(dirLight);
window.addEventListener('resize', onWindowResize, false);
}
// Función auxiliar para construir paredes del mapa
function addWall(x, z, width, depth, color = 0x1e293b) {
const height = 4;
const geo = new THREE.BoxGeometry(width, height, depth);
const mat = new THREE.MeshStandardMaterial({
color: color,
roughness: 0.6,
metalness: 0.2
});
const wall = new THREE.Mesh(geo, mat);
wall.position.set(x, height / 2, z);
wall.castShadow = true;
wall.receiveShadow = true;
scene.add(wall);
// Guardar caja de colisión delimitadora
const box = new THREE.Box3().setFromObject(wall);
wallColliders.push(box);
// Marco o zócalo visual superior
const borderGeo = new THREE.BoxGeometry(width + 0.05, 0.2, depth + 0.05);
const borderMat = new THREE.MeshStandardMaterial({ color: 0x3b82f6 });
const border = new THREE.Mesh(borderGeo, borderMat);
border.position.set(x, height, z);
scene.add(border);
}
// Crear Puerta Automática que se abre al acercarse
function createDoor(x, z, width, depth, isHorizontal = true) {
const doorGroup = new THREE.Group();
doorGroup.position.set(x, 0, z);
const height = 3.5;
const doorGeo = new THREE.BoxGeometry(
isHorizontal ? width : depth,
height,
isHorizontal ? 0.15 : width
);
const doorMat = new THREE.MeshStandardMaterial({
color: 0xda70d6,
roughness: 0.3,
metalness: 0.5,
emissive: 0x4a0e4e,
emissiveIntensity: 0.2
});
// Hoja de la puerta
const doorMesh = new THREE.Mesh(doorGeo, doorMat);
doorMesh.position.y = height / 2;
doorMesh.castShadow = true;
doorGroup.add(doorMesh);
scene.add(doorGroup);
// Guardar la estructura de la puerta para su control animado y físico
const doorObj = {
group: doorGroup,
mesh: doorMesh,
position: new THREE.Vector3(x, 0, z),
triggerRadius: 4.0, // Distancia de apertura automática
currentOpen: 0, // 0 = Cerrada, 1 = Totalmente Abierta
targetOpen: 0,
width: width,
height: height,
isHorizontal: isHorizontal,
colliderBox: new THREE.Box3()
};
updateDoorCollider(doorObj);
doors.push(doorObj);
}
function updateDoorCollider(doorObj) {
// Si la puerta está abierta casi del todo, quitamos su colisión para permitir el paso
if (doorObj.currentOpen > 0.8) {
doorObj.colliderBox.makeEmpty();
} else {
doorObj.colliderBox.setFromObject(doorObj.mesh);
}
}
// Construcción completa del mapa con Pasillos y Habitaciones
function buildMap() {
// 1. Suelo General con patrón de baldosas
const floorGeo = new THREE.PlaneGeometry(80, 80);
const floorMat = new THREE.MeshStandardMaterial({
color: 0x0f172a,
roughness: 0.8,
metalness: 0.1
});
const floor = new THREE.Mesh(floorGeo, floorMat);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);
// Rejilla visual sobre el suelo
const grid = new THREE.GridHelper(80, 40, 0x3b82f6, 0x1e293b);
grid.position.y = 0.01;
scene.add(grid);
// 2. PAREDES EXTERIORES (Límite Global)
addWall(0, -30, 60, 1);
addWall(0, 30, 60, 1);
addWall(-30, 0, 1, 60);
addWall(30, 0, 1, 60);
// 3. HABITACIÓN 1 (Norte)
addWall(-10, -18, 16, 1);
addWall(10, -18, 16, 1);
addWall(-18, -24, 1, 12);
addWall(18, -24, 1, 12);
createDoor(0, -18, 4, 1, true); // Puerta de acceso Norte
// 4. HABITACIÓN 2 (Sur)
addWall(-10, 18, 16, 1);
addWall(10, 18, 16, 1);
addWall(-18, 24, 1, 12);
addWall(18, 24, 1, 12);
createDoor(0, 18, 4, 1, true); // Puerta de acceso Sur
// 5. PASILLOS CENTRALES Y HABITACIÓN ESTE
addWall(16, -6, 1, 14);
addWall(16, 6, 1, 14);
addWall(24, 0, 12, 1);
createDoor(16, 0, 1, 4, false); // Puerta de acceso Este
// 6. HABITACIÓN OESTE
addWall(-16, -6, 1, 14);
addWall(-16, 6, 1, 14);
addWall(-24, 0, 12, 1);
createDoor(-16, 0, 1, 4, false); // Puerta de acceso Oeste
}
// Creación de la Farola (Luz que sigue al muñeco desde una elevación)
function createStreetlight() {
streetlightGroup = new THREE.Group();
const poleMat = new THREE.MeshStandardMaterial({ color: 0x334155, metalness: 0.8 });
const poleGeo = new THREE.CylinderGeometry(0.1, 0.15, 7, 16);
const pole = new THREE.Mesh(poleGeo, poleMat);
pole.position.y = 3.5;
streetlightGroup.add(pole);
const lampHeadGeo = new THREE.ConeGeometry(0.5, 0.5, 16);
lampHeadGeo.rotateX(Math.PI);
const lampHead = new THREE.Mesh(lampHeadGeo, poleMat);
lampHead.position.set(0, 7, 0);
streetlightGroup.add(lampHead);
// Bombilla brillante
const bulbGeo = new THREE.SphereGeometry(0.25, 16, 16);
const bulbMat = new THREE.MeshBasicMaterial({ color: 0xfef08a });
const bulb = new THREE.Mesh(bulbGeo, bulbMat);
bulb.position.set(0, 6.8, 0);
streetlightGroup.add(bulb);
scene.add(streetlightGroup);
// Spotlight (Foco directo sobre el personaje)
spotlight = new THREE.SpotLight(0xfffbe6, 4);
spotlight.position.set(0, 7, 0);
spotlight.angle = Math.PI / 3;
spotlight.penumbra = 0.3;
spotlight.decay = 1;
spotlight.distance = 40;
spotlight.castShadow = true;
spotlight.shadow.mapSize.width = 1024;
spotlight.shadow.mapSize.height = 1024;
scene.add(spotlight);
}
// Creación del Monigote 3D del Jugador
function createMonigote() {
monigoteGroup = new THREE.Group();
const bodyMat = new THREE.MeshStandardMaterial({
color: 0x020617,
roughness: 0.3,
metalness: 0.2
});
// Cabeza
const headGeo = new THREE.SphereGeometry(0.35, 24, 24);
const head = new THREE.Mesh(headGeo, bodyMat);
head.position.y = 1.65;
head.castShadow = true;
monigoteGroup.add(head);
// Torso
const torsoGeo = new THREE.CylinderGeometry(0.08, 0.08, 0.85, 12);
const torso = new THREE.Mesh(torsoGeo, bodyMat);
torso.position.y = 0.95;
torso.castShadow = true;
monigoteGroup.add(torso);
// Extremidades articuladas
const createLimb = (length) => {
const joint = new THREE.Group();
const meshGeo = new THREE.CylinderGeometry(0.05, 0.05, length, 12);
meshGeo.translate(0, -length / 2, 0);
const mesh = new THREE.Mesh(meshGeo, bodyMat);
mesh.castShadow = true;
joint.add(mesh);
return joint;
};
limbs.armL = createLimb(0.7);
limbs.armL.position.set(-0.18, 1.3, 0);
monigoteGroup.add(limbs.armL);
limbs.armR = createLimb(0.7);
limbs.armR.position.set(0.18, 1.3, 0);
monigoteGroup.add(limbs.armR);
limbs.legL = createLimb(0.8);
limbs.legL.position.set(-0.12, 0.55, 0);
monigoteGroup.add(limbs.legL);
limbs.legR = createLimb(0.8);
limbs.legR.position.set(0.12, 0.55, 0);
monigoteGroup.add(limbs.legR);
monigoteGroup.position.copy(characterState.position);
scene.add(monigoteGroup);
}
// Creación de Monigotes Enemigos (Color Rojo)
function spawnEnemies(count) {
const spawnPoints = [
{ x: 0, z: -22 }, { x: -22, z: 0 }, { x: 22, z: 0 }, { x: 0, z: 22 },
{ x: -10, z: -10 }, { x: 10, z: -10 }, { x: -10, z: 10 }, { x: 10, z: 10 },
{ x: -22, z: -22 }, { x: 22, z: 22 }
];
for (let i = 0; i < count; i++) {
const group = new THREE.Group();
const enemyMat = new THREE.MeshStandardMaterial({
color: 0xef4444, // Rojo intenso
roughness: 0.4,
metalness: 0.1
});
// Cabeza
const headGeo = new THREE.SphereGeometry(0.35, 16, 16);
const head = new THREE.Mesh(headGeo, enemyMat);
head.position.y = 1.65;
head.castShadow = true;
group.add(head);
// Torso
const torsoGeo = new THREE.CylinderGeometry(0.08, 0.08, 0.85, 12);
const torso = new THREE.Mesh(torsoGeo, enemyMat);
torso.position.y = 0.95;
torso.castShadow = true;
group.add(torso);
// Extremidades
const createLimb = (length) => {
const joint = new THREE.Group();
const meshGeo = new THREE.CylinderGeometry(0.05, 0.05, length, 10);
meshGeo.translate(0, -length / 2, 0);
const mesh = new THREE.Mesh(meshGeo, enemyMat);
mesh.castShadow = true;
joint.add(mesh);
return joint;
};
const armL = createLimb(0.7); armL.position.set(-0.18, 1.3, 0); group.add(armL);
const armR = createLimb(0.7); armR.position.set(0.18, 1.3, 0); group.add(armR);
const legL = createLimb(0.8); legL.position.set(-0.12, 0.55, 0); group.add(legL);
const legR = createLimb(0.8); legR.position.set(0.12, 0.55, 0); group.add(legR);
const pos = spawnPoints[i % spawnPoints.length];
group.position.set(pos.x, 0, pos.z);
scene.add(group);
// Ángulo de movimiento aleatorio
const angle = Math.random() * Math.PI * 2;
const dir = new THREE.Vector3(Math.sin(angle), 0, Math.cos(angle)).normalize();
enemies.push({
group: group,
materials: [enemyMat],
position: group.position,
direction: dir,
rotation: angle,
walkCycle: Math.random() * 10,
limbs: { armL, armR, legL, legR },
hit: false, // Si ha sido tocado por una bola verde
shrinking: false, // Animación de encogimiento activa
scale: 1.0,
radius: 0.35
});
}
}
// Lanzamiento de bola verde desde la cabeza del personaje
function shootGreenBall() {
if (gameWon) return;
const ballGeo = new THREE.SphereGeometry(0.25, 16, 16);
const ballMat = new THREE.MeshStandardMaterial({
color: 0x10b981,
emissive: 0x34d399,
emissiveIntensity: 0.9,
roughness: 0.2
});
const ballMesh = new THREE.Mesh(ballGeo, ballMat);
// Posición inicial: Cabeza del monigote jugable
const headPos = characterState.position.clone();
headPos.y = 1.65;
ballMesh.position.copy(headPos);
// Orientación basada en la mirada actual del muñeco
const dir = new THREE.Vector3(
Math.sin(characterState.rotation),
0,
Math.cos(characterState.rotation)
).normalize();
// Pequeño desplazamiento frontal para salir fuera de la cabeza
ballMesh.position.addScaledVector(dir, 0.4);
scene.add(ballMesh);
projectiles.push({
mesh: ballMesh,
position: ballMesh.position,
direction: dir,
speed: 18.0,
life: 2.5 // Duración en segundos antes de ser eliminada
});
}
function setupControls() {
window.addEventListener('keydown', (e) => {
const key = e.key.toLowerCase();
if (keys.hasOwnProperty(key)) {
keys[key] = true;
const keyEl = document.getElementById(`key-${key}`);
if (keyEl) keyEl.classList.add('bg-amber-500', 'text-slate-900', 'border-amber-400');
}
if (e.code === 'Space') {
e.preventDefault();
shootGreenBall();
}
});
window.addEventListener('keyup', (e) => {
const key = e.key.toLowerCase();
if (keys.hasOwnProperty(key)) {
keys[key] = false;
const keyEl = document.getElementById(`key-${key}`);
if (keyEl) keyEl.classList.remove('bg-amber-500', 'text-slate-900', 'border-amber-400');
}
});
// Permite disparar también haciendo clic en la pantalla
window.addEventListener('pointerdown', (e) => {
if (e.target.tagName !== 'INPUT' && e.target.tagName !== 'BUTTON') {
shootGreenBall();
}
});
// Sliders de velocidad
const slider = document.getElementById('speedSlider');
const speedDisplay = document.getElementById('speedValue');
if (slider && speedDisplay) {
slider.addEventListener('input', (e) => {
characterState.speed = parseFloat(e.target.value);
speedDisplay.textContent = characterState.speed.toFixed(1);
});
}
const enemySlider = document.getElementById('enemySpeedSlider');
const enemySpeedDisplay = document.getElementById('enemySpeedValue');
if (enemySlider && enemySpeedDisplay) {
enemySlider.addEventListener('input', (e) => {
globalEnemySpeed = parseFloat(e.target.value);
enemySpeedDisplay.textContent = globalEnemySpeed.toFixed(1);
});
}
// Botón de reiniciar partida
const restartBtn = document.getElementById('restartBtn');
if (restartBtn) {
restartBtn.addEventListener('click', () => {
location.reload();
});
}
}
// Gestión del movimiento del jugador
function updateCharacter(delta) {
let moveX = 0;
let moveZ = 0;
if (keys.w) moveZ -= 1;
if (keys.s) moveZ += 1;
if (keys.a) moveX -= 1;
if (keys.d) moveX += 1;
const moveDir = new THREE.Vector3(moveX, 0, moveZ);
characterState.isMoving = moveDir.lengthSq() > 0;
if (characterState.isMoving) {
moveDir.normalize();
const stepDistance = characterState.speed * delta;
const nextPosX = characterState.position.x + moveDir.x * stepDistance;
const nextPosZ = characterState.position.z + moveDir.z * stepDistance;
if (!checkCollisions(nextPosX, characterState.position.z)) {
characterState.position.x = nextPosX;
}
if (!checkCollisions(characterState.position.x, nextPosZ)) {
characterState.position.z = nextPosZ;
}
const targetRotation = Math.atan2(moveDir.x, moveDir.z);
let diff = targetRotation - characterState.rotation;
diff = Math.atan2(Math.sin(diff), Math.cos(diff));
characterState.rotation += diff * 0.2;
characterState.walkCycle += delta * characterState.speed * 2.8;
const swing = Math.sin(characterState.walkCycle) * 0.7;
limbs.legL.rotation.x = swing;
limbs.legR.rotation.x = -swing;
limbs.armL.rotation.x = -swing * 0.8;
limbs.armR.rotation.x = swing * 0.8;
monigoteGroup.position.y = Math.abs(Math.sin(characterState.walkCycle * 2)) * 0.08;
} else {
limbs.legL.rotation.x *= 0.8;
limbs.legR.rotation.x *= 0.8;
limbs.armL.rotation.x *= 0.8;
limbs.armR.rotation.x *= 0.8;
monigoteGroup.position.y = 0;
}
monigoteGroup.position.x = characterState.position.x;
monigoteGroup.position.z = characterState.position.z;
monigoteGroup.rotation.y = characterState.rotation;
}
// IA y movimiento autónomo de los enemigos rojos
function updateEnemies(delta) {
for (let i = enemies.length - 1; i >= 0; i--) {
const enemy = enemies[i];
// Si está en proceso de encogimiento tras ser alcanzado por bola verde
if (enemy.shrinking) {
enemy.scale -= delta * 1.5;
if (enemy.scale <= 0) {
scene.remove(enemy.group);
enemies.splice(i, 1);
score++;
updateScoreUI();
continue;
}
enemy.group.scale.set(enemy.scale, enemy.scale, enemy.scale);
continue;
}
// Avance continuo del enemigo
const step = globalEnemySpeed * delta;
const nextX = enemy.position.x + enemy.direction.x * step;
const nextZ = enemy.position.z + enemy.direction.z * step;
// Si colisiona con pared o puerta, rebota hacia un nuevo ángulo
if (checkCollisions(nextX, nextZ)) {
const randomAngle = Math.random() * Math.PI * 2;
enemy.direction.set(Math.sin(randomAngle), 0, Math.cos(randomAngle)).normalize();
} else {
enemy.position.x = nextX;
enemy.position.z = nextZ;
}
// Rotación suave del enemigo en su dirección
const targetRot = Math.atan2(enemy.direction.x, enemy.direction.z);
enemy.rotation += (targetRot - enemy.rotation) * 0.1;
enemy.group.rotation.y = enemy.rotation;
// Animación de extremidades al caminar
enemy.walkCycle += delta * globalEnemySpeed * 2.5;
const swing = Math.sin(enemy.walkCycle) * 0.6;
enemy.limbs.legL.rotation.x = swing;
enemy.limbs.legR.rotation.x = -swing;
enemy.limbs.armL.rotation.x = -swing * 0.7;
enemy.limbs.armR.rotation.x = swing * 0.7;
}
}
// Actualización y física de proyectiles
function updateProjectiles(delta) {
for (let i = projectiles.length - 1; i >= 0; i--) {
const proj = projectiles[i];
proj.life -= delta;
if (proj.life <= 0) {
scene.remove(proj.mesh);
projectiles.splice(i, 1);
continue;
}
const dist = proj.speed * delta;
const nextPos = proj.position.clone().addScaledVector(proj.direction, dist);
// Colisión con paredes
if (checkCollisions(nextPos.x, nextPos.z)) {
scene.remove(proj.mesh);
projectiles.splice(i, 1);
continue;
}
proj.position.copy(nextPos);
// Comprobar colisión con enemigos
for (let j = 0; j < enemies.length; j++) {
const enemy = enemies[j];
if (!enemy.hit && !enemy.shrinking) {
const distToEnemy = proj.position.distanceTo(enemy.position.clone().setY(1.0));
if (distToEnemy < 0.7) {
// Convertir enemigo a verde e iniciar encogimiento
enemy.hit = true;
enemy.shrinking = true;
// Cambiar material a verde esmeralda brillante
enemy.materials.forEach(mat => {
mat.color.setHex(0x10b981);
mat.emissive.setHex(0x059669);
mat.emissiveIntensity = 0.5;
});
// Eliminar la bola
scene.remove(proj.mesh);
projectiles.splice(i, 1);
break;
}
}
}
}
}
function updateScoreUI() {
const badge = document.getElementById('scoreBadge');
if (badge) badge.textContent = `${score} / ${TOTAL_ENEMIES}`;
if (score >= TOTAL_ENEMIES && !gameWon) {
gameWon = true;
const modal = document.getElementById('victoryModal');
if (modal) modal.classList.remove('hidden');
}
}
// Comprobación de Colisiones sólidas (Paredes y Puertas cerradas)
function checkCollisions(posX, posZ) {
const playerRadius = characterState.radius;
// Check con paredes rígidas
for (let i = 0; i < wallColliders.length; i++) {
const box = wallColliders[i];
if (posX + playerRadius > box.min.x && posX - playerRadius < box.max.x &&
posZ + playerRadius > box.min.z && posZ - playerRadius < box.max.z) {
return true;
}
}
// Check con puertas cuando están cerradas
for (let i = 0; i < doors.length; i++) {
const door = doors[i];
if (door.currentOpen < 0.7) {
const box = door.colliderBox;
if (!box.isEmpty()) {
if (posX + playerRadius > box.min.x && posX - playerRadius < box.max.x &&
posZ + playerRadius > box.min.z && posZ - playerRadius < box.max.z) {
return true;
}
}
}
}
return false;
}
// Actualización de Animación de Puertas Automáticas
function updateDoors(delta) {
let doorOpeningNear = false;
doors.forEach(door => {
const dist = characterState.position.distanceTo(door.position);
if (dist < door.triggerRadius) {
door.targetOpen = 1;
doorOpeningNear = true;
} else {
door.targetOpen = 0;
}
door.currentOpen += (door.targetOpen - door.currentOpen) * (delta * 5);
door.mesh.position.y = (door.height / 2) + (door.currentOpen * (door.height - 0.2));
updateDoorCollider(door);
});
const doorAlertEl = document.getElementById('doorAlert');
if (doorAlertEl) {
doorAlertEl.style.opacity = doorOpeningNear ? '1' : '0';
}
}
// Actualización de Cámara y Farola
function updateCameraAndStreetlight() {
const targetPos = monigoteGroup.position.clone();
streetlightGroup.position.set(targetPos.x - 2, 0, targetPos.z - 2);
spotlight.position.set(targetPos.x - 2, 7, targetPos.z - 2);
spotlight.target.position.copy(targetPos);
spotlight.target.updateMatrixWorld();
camera.position.set(targetPos.x - 4, 9, targetPos.z + 7);
camera.lookAt(targetPos.x, 1, targetPos.z);
}
// Bucle Principal de Renderizado
let lastTime = 0;
function animate(currentTime) {
requestAnimationFrame(animate);
const delta = Math.min((currentTime - lastTime) / 1000, 0.1) || 0;
lastTime = currentTime;
updateCharacter(delta);
updateEnemies(delta);
updateProjectiles(delta);
updateDoors(delta);
updateCameraAndStreetlight();
renderer.render(scene, camera);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
</script>
</body>
</html>
