lunes, 9 de noviembre de 2026

Juego de monigote atacando muchos monigotes solo IA;

https://sites.google.com/view/juego-monigote-3d/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 / 80</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 eliminado a 80 enemigos rojos con tus proyectiles verdes y asegurado 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 TARGET_SCORE = 80;
        const TOTAL_ENEMIES = 100;
        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) {
            // Generar área de aparición distribuida por los pasillos y habitaciones (-26 a 26 en X y Z)
            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);

                // Posicionamiento aleatorio en área válida lejos del centro de inicio
                let spawnX, spawnZ;
                do {
                    spawnX = (Math.random() - 0.5) * 52;
                    spawnZ = (Math.random() - 0.5) * 52;
                } while (Math.hypot(spawnX - characterState.position.x, spawnZ - characterState.position.z) < 4);

                group.position.set(spawnX, 0, spawnZ);
                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} / ${TARGET_SCORE}`;

            if (score >= TARGET_SCORE && !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>

No hay comentarios:

Publicar un comentario