lunes, 9 de noviembre de 2026

Juego echo solo con IA se calibra casi todo con el mouse para jugar a gusto del jugador;

https://sites.google.com/view/disparos-a-todos/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>Juego 3D Monigote en Pasillos Cilíndricos</title>

    <!-- Tailwind CSS -->

    <script src="https://cdn.tailwindcss.com"></script>

    <!-- Three.js Library -->

    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>

    <style>

        body {

            margin: 0;

            padding: 0;

            overflow: hidden;

            font-family: 'Inter', sans-serif;

            background-color: #090d16;

            user-select: none;

        }

        #canvas-container {

            width: 100vw;

            height: 100vh;

            display: block;

        }

    </style>

</head>

<body class="relative w-screen h-screen overflow-hidden bg-slate-950">


    <!-- Canvas para Three.js -->

    <div id="canvas-container"></div>


    <!-- UI Overlay - Panel Superior Izquierdo -->

    <div class="absolute top-4 left-4 z-10 bg-slate-900/90 backdrop-blur-md border border-slate-700/60 p-4 rounded-2xl shadow-2xl text-white 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 <kbd class="px-1 py-0.5 bg-slate-700 rounded text-amber-300 font-mono">WASD</kbd> 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.

        </p>


        <!-- Barra de Vida del Jugador -->

        <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 text-xs font-semibold">

                <span class="text-rose-400 uppercase tracking-wider flex items-center gap-1">❤️ Vida</span>

                <span id="healthValue" class="font-mono text-emerald-400 font-bold">100 / 100</span>

            </div>

            <div class="w-full bg-slate-700/80 rounded-full h-3.5 p-0.5 border border-slate-600">

                <div id="healthBar" class="bg-gradient-to-r from-emerald-500 to-teal-400 h-full rounded-full transition-all duration-200 shadow-sm" style="width: 100%;"></div>

            </div>

        </div>


        <!-- Controles visuales WASD -->

        <div class="mb-3 bg-slate-800/60 p-2.5 rounded-lg border border-slate-700/40">

            <span class="text-[10px] uppercase font-bold text-slate-400 tracking-wider block mb-2">Controles Teclado</span>

            <div class="grid grid-cols-3 gap-1 w-28 mx-auto text-center font-mono font-bold text-xs">

                <div></div>

                <div id="key-w" class="bg-slate-700 border border-slate-600 py-1 rounded text-slate-200 transition-colors">W</div>

                <div></div>

                <div id="key-a" class="bg-slate-700 border border-slate-600 py-1 rounded text-slate-200 transition-colors">A</div>

                <div id="key-s" class="bg-slate-700 border border-slate-600 py-1 rounded text-slate-200 transition-colors">S</div>

                <div id="key-d" class="bg-slate-700 border border-slate-600 py-1 rounded text-slate-200 transition-colors">D</div>

            </div>

        </div>


        <!-- Controles de Velocidad por Sliders -->

        <div class="space-y-2">

            <div>

                <div class="flex justify-between items-center text-xs mb-1">

                    <span class="text-slate-300 font-medium">Velocidad Jugador</span>

                    <span id="speedValue" class="text-amber-400 font-mono font-bold">5.0</span>

                </div>

                <input id="speedSlider" type="range" min="1" max="12" step="0.5" value="5.0" 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-1">

                    <span class="text-slate-300 font-medium">Velocidad Enemigos</span>

                    <span id="enemySpeedValue" class="text-rose-400 font-mono font-bold">3.0</span>

                </div>

                <input id="enemySpeedSlider" type="range" min="0.5" max="8" step="0.5" value="3.0" class="w-full h-1.5 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-rose-500">

            </div>

            <div>

                <div class="flex justify-between items-center text-xs mb-1">

                    <span class="text-slate-300 font-medium">Vel. Disparo Enemigos</span>

                    <span id="enemyShootSpeedValue" class="text-orange-400 font-mono font-bold">5.0</span>

                </div>

                <input id="enemyShootSpeedSlider" type="range" min="0" max="10" step="0.5" value="5.0" class="w-full h-1.5 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-orange-500">

            </div>

        </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">

            <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.

            </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>


    <!-- Modal de Derrota -->

    <div id="gameOverModal" 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-rose-500/40 p-8 rounded-2xl max-w-sm w-full text-center shadow-2xl space-y-4">

            <div class="text-6xl animate-pulse">💀</div>

            <h2 class="text-3xl font-black text-rose-500 tracking-wide">¡JUEGO PERDIDO!</h2>

            <p class="text-slate-300 text-sm leading-relaxed">

                Los enemigos te han disparado hasta agotar toda tu vida (0 HP).

            </p>

            <button id="restartGameOverBtn" class="w-full py-3 bg-gradient-to-r from-rose-600 to-red-500 hover:from-rose-500 hover:to-red-400 text-white font-black tracking-wider uppercase rounded-xl shadow-lg transition-transform active:scale-95">

                Juega Otra Vez

            </button>

        </div>

    </div>


    <!-- Código Principal Three.js -->

    <script>

        let scene, camera, renderer;

        let monigoteGroup, limbs = {};

        let spotlight;


        const wallColliders = [];

        const doors = [];

        const enemies = [];

        const projectiles = [];

        const enemyProjectiles = [];


        let score = 0;

        const TARGET_SCORE = 80;

        const TOTAL_ENEMIES = 100;

        let globalEnemySpeed = 3.0;

        let enemyShootSpeedFactor = 5.0;

        let gameWon = false;

        let gameOver = false;


        let playerHealth = 100;

        const MAX_HEALTH = 100;


        const characterState = {

            position: new THREE.Vector3(0, 0, 0),

            rotation: 0,

            speed: 5.0,

            walkCycle: 0

        };


        const keys = { w: false, a: false, s: false, d: false };


        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');

                }

            });


            window.addEventListener('pointerdown', (e) => {

                if (e.target.tagName !== 'INPUT' && e.target.tagName !== 'BUTTON') {

                    shootGreenBall();

                }

            });


            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);

                });

            }


            const enemyShootSlider = document.getElementById('enemyShootSpeedSlider');

            const enemyShootSpeedDisplay = document.getElementById('enemyShootSpeedValue');

            if (enemyShootSlider && enemyShootSpeedDisplay) {

                enemyShootSlider.addEventListener('input', (e) => {

                    enemyShootSpeedFactor = parseFloat(e.target.value);

                    enemyShootSpeedDisplay.textContent = enemyShootSpeedFactor.toFixed(1);

                });

            }


            const restartBtn = document.getElementById('restartBtn');

            if (restartBtn) restartBtn.addEventListener('click', () => location.reload());


            const restartGameOverBtn = document.getElementById('restartGameOverBtn');

            if (restartGameOverBtn) restartGameOverBtn.addEventListener('click', () => location.reload());

        }


        function initScene() {

            const container = document.getElementById('canvas-container');

            scene = new THREE.Scene();

            scene.background = new THREE.Color(0x050811);

            scene.fog = new THREE.FogExp2(0x050811, 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;

            container.appendChild(renderer.domElement);


            const ambientLight = new THREE.AmbientLight(0x223344, 0.7);

            scene.add(ambientLight);


            spotlight = new THREE.SpotLight(0xfff5ea, 3.0);

            spotlight.angle = Math.PI / 3.5;

            spotlight.penumbra = 0.5;

            spotlight.decay = 1.5;

            spotlight.distance = 60;

            spotlight.castShadow = true;

            scene.add(spotlight);


            buildLevel();

            monigoteGroup = createStickman(0x38bdf8);

            scene.add(monigoteGroup);


            spawnEnemies(TOTAL_ENEMIES);

            setupControls();


            window.addEventListener('resize', onWindowResize);

        }


        function buildLevel() {

            const floorGeo = new THREE.PlaneGeometry(120, 120);

            const floorMat = new THREE.MeshStandardMaterial({ color: 0x0f172a, roughness: 0.8 });

            const floor = new THREE.Mesh(floorGeo, floorMat);

            floor.rotation.x = -Math.PI / 2;

            floor.receiveShadow = true;

            scene.add(floor);


            const grid = new THREE.GridHelper(120, 60, 0x3b82f6, 0x1e293b);

            grid.position.y = 0.01;

            scene.add(grid);


            // Paredes exteriores del mapa

            addWallBlock(0, 2.5, -30, 60, 5, 1);

            addWallBlock(0, 2.5, 30, 60, 5, 1);

            addWallBlock(-30, 2.5, 0, 1, 5, 60);

            addWallBlock(30, 2.5, 0, 1, 5, 60);


            // Laberinto interno

            addWallBlock(-10, 2.5, -15, 20, 5, 1);

            addWallBlock(15, 2.5, -10, 1, 5, 20);

            addWallBlock(-15, 2.5, 10, 1, 5, 20);

            addWallBlock(10, 2.5, 15, 20, 5, 1);


            // Puertas automáticas

            createDoor(0, 0, -15, 4, false);

            createDoor(15, 0, 5, 4, true);

            createDoor(-15, 0, -5, 4, true);

            createDoor(0, 0, 15, 4, false);

        }


        function addWallBlock(x, y, z, w, h, d) {

            const geo = new THREE.BoxGeometry(w, h, d);

            const mat = new THREE.MeshStandardMaterial({ color: 0x1e293b, roughness: 0.6 });

            const wall = new THREE.Mesh(geo, mat);

            wall.position.set(x, y, z);

            wall.castShadow = true;

            wall.receiveShadow = true;

            scene.add(wall);


            wallColliders.push(new THREE.Box3().setFromObject(wall));

        }


        function createDoor(x, y, z, width, isVertical) {

            const doorGroup = new THREE.Group();

            doorGroup.position.set(x, y, z);


            const panelGeo = new THREE.BoxGeometry(isVertical ? 0.3 : width / 2, 4.5, isVertical ? width / 2 : 0.3);

            const panelMat = new THREE.MeshStandardMaterial({ color: 0x0284c7, roughness: 0.3, metalness: 0.5 });


            const leftPanel = new THREE.Mesh(panelGeo, panelMat);

            const rightPanel = new THREE.Mesh(panelGeo, panelMat);


            if (isVertical) {

                leftPanel.position.z = -width / 4;

                rightPanel.position.z = width / 4;

            } else {

                leftPanel.position.x = -width / 4;

                rightPanel.position.x = width / 4;

            }


            doorGroup.add(leftPanel);

            doorGroup.add(rightPanel);

            scene.add(doorGroup);


            doors.push({

                group: doorGroup,

                left: leftPanel,

                right: rightPanel,

                isVertical: isVertical,

                width: width,

                openFactor: 0,

                position: new THREE.Vector3(x, y, z)

            });

        }


        function createStickman(colorHex) {

            const group = new THREE.Group();

            const mat = new THREE.MeshStandardMaterial({ color: colorHex, roughness: 0.4 });


            // Cabeza

            const head = new THREE.Mesh(new THREE.SphereGeometry(0.35, 16, 16), mat);

            head.position.y = 1.65;

            group.add(head);


            // Torso

            const torso = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.08, 0.7), mat);

            torso.position.y = 1.1;

            group.add(torso);


            // Extremidades con articulaciones con pivote en hombros y caderas

            const createLimb = (xPos, yPos) => {

                const limbGroup = new THREE.Group();

                limbGroup.position.set(xPos, yPos, 0);

                const mesh = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 0.6), mat);

                mesh.position.y = -0.3; // Desplazar centro para rotar desde el extremo

                limbGroup.add(mesh);

                return limbGroup;

            };


            const armL = createLimb(-0.2, 1.35);

            const armR = createLimb(0.2, 1.35);

            const legL = createLimb(-0.15, 0.75);

            const legR = createLimb(0.15, 0.75);


            group.add(armL); group.add(armR); group.add(legL); group.add(legR);


            // Guardar referencias a las articulaciones si es el personaje del jugador

            if (colorHex === 0x38bdf8) {

                limbs = { armL, armR, legL, legR };

            }


            return group;

        }


        function spawnEnemies(count) {

            for (let i = 0; i < count; i++) {

                let rx, rz;

                do {

                    rx = (Math.random() - 0.5) * 50;

                    rz = (Math.random() - 0.5) * 50;

                } while (Math.abs(rx) < 4 && Math.abs(rz) < 4);


                const enemyMat = new THREE.MeshStandardMaterial({ color: 0xef4444, roughness: 0.4 });

                const group = createStickman(0xef4444);

                group.position.set(rx, 0, rz);

                scene.add(group);


                const angle = Math.random() * Math.PI * 2;

                const dir = new THREE.Vector3(Math.sin(angle), 0, Math.cos(angle)).normalize();


                // Guardar referencias individuales a las extremidades del enemigo

                const armL = group.children[2];

                const armR = group.children[3];

                const legL = group.children[4];

                const legR = group.children[5];


                enemies.push({

                    group: group,

                    materials: [enemyMat],

                    position: group.position,

                    direction: dir,

                    rotation: angle,

                    walkCycle: Math.random() * 10,

                    limbs: { armL, armR, legL, legR },

                    hit: false,

                    shrinking: false,

                    scale: 1.0,

                    shootCooldown: Math.random() * 2 + 1.5

                });

            }

        }


        function shootGreenBall() {

            if (gameWon || gameOver) return;


            const ballGeo = new THREE.SphereGeometry(0.25, 16, 16);

            const ballMat = new THREE.MeshStandardMaterial({ color: 0x10b981, emissive: 0x059669, emissiveIntensity: 0.8 });

            const ballMesh = new THREE.Mesh(ballGeo, ballMat);


            const spawnPos = characterState.position.clone();

            spawnPos.y = 1.7;

            ballMesh.position.copy(spawnPos);


            const dir = new THREE.Vector3(Math.sin(characterState.rotation), 0, Math.cos(characterState.rotation)).normalize();

            scene.add(ballMesh);


            projectiles.push({

                mesh: ballMesh,

                position: ballMesh.position,

                direction: dir,

                speed: 16.0,

                life: 3.0

            });

        }


        function enemyShoot(enemy) {

            if (gameOver || gameWon) return;

            if (enemyShootSpeedFactor <= 0) return; // Si la velocidad es 0, no disparan


            const ballGeo = new THREE.SphereGeometry(0.2, 16, 16);

            const ballMat = new THREE.MeshStandardMaterial({ color: 0xef4444, emissive: 0xf87171, emissiveIntensity: 0.9 });

            const ballMesh = new THREE.Mesh(ballGeo, ballMat);


            const spawnPos = enemy.position.clone();

            spawnPos.y = 1.65;

            ballMesh.position.copy(spawnPos);


            const playerPos = characterState.position.clone().setY(1.0);

            const dir = new THREE.Vector3().subVectors(playerPos, spawnPos).normalize();


            scene.add(ballMesh);


            enemyProjectiles.push({

                mesh: ballMesh,

                position: ballMesh.position,

                direction: dir,

                speedMultiplier: enemyShootSpeedFactor,

                life: 3.0

            });

        }


        function updateProjectiles(delta) {

            // Bolas verdes del jugador

            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);


                if (checkCollisions(nextPos.x, nextPos.z)) {

                    scene.remove(proj.mesh);

                    projectiles.splice(i, 1);

                    continue;

                }


                proj.position.copy(nextPos);


                // Comprobar impacto con enemigos

                for (let j = enemies.length - 1; j >= 0; j--) {

                    const enemy = enemies[j];

                    if (enemy.hit) continue;


                    const distToEnemy = proj.position.distanceTo(enemy.position.clone().setY(1.0));

                    if (distToEnemy < 0.8) {

                        enemy.hit = true;

                        enemy.shrinking = true;


                        // Cambiar color a verde al impactar

                        enemy.group.traverse((child) => {

                            if (child.isMesh) {

                                child.material = new THREE.MeshStandardMaterial({

                                    color: 0x10b981,

                                    emissive: 0x059669,

                                    emissiveIntensity: 0.5

                                });

                            }

                        });


                        score++;

                        updateScoreUI();


                        scene.remove(proj.mesh);

                        projectiles.splice(i, 1);

                        break;

                    }

                }

            }


            // Bolas rojas de los enemigos

            for (let i = enemyProjectiles.length - 1; i >= 0; i--) {

                const proj = enemyProjectiles[i];

                proj.life -= delta;


                if (proj.life <= 0) {

                    scene.remove(proj.mesh);

                    enemyProjectiles.splice(i, 1);

                    continue;

                }


                const currentSpeed = 2.4 * enemyShootSpeedFactor;

                const dist = currentSpeed * delta;

                const nextPos = proj.position.clone().addScaledVector(proj.direction, dist);


                if (checkCollisions(nextPos.x, nextPos.z)) {

                    scene.remove(proj.mesh);

                    enemyProjectiles.splice(i, 1);

                    continue;

                }


                proj.position.copy(nextPos);


                if (!gameOver && !gameWon) {

                    const distToPlayer = proj.position.distanceTo(characterState.position.clone().setY(1.0));

                    if (distToPlayer < 0.65) {

                        takeDamage(10);

                        scene.remove(proj.mesh);

                        enemyProjectiles.splice(i, 1);

                    }

                }

            }

        }


        function takeDamage(amount) {

            if (gameOver || gameWon) return;


            playerHealth = Math.max(0, playerHealth - amount);

            const healthBar = document.getElementById('healthBar');

            const healthText = document.getElementById('healthValue');


            if (healthBar && healthText) {

                const percent = (playerHealth / MAX_HEALTH) * 100;

                healthBar.style.width = `${percent}%`;

                healthText.textContent = `${playerHealth} / ${MAX_HEALTH}`;


                if (percent <= 25) {

                    healthBar.className = "bg-gradient-to-r from-rose-600 to-red-500 h-full rounded-full transition-all duration-200 shadow-sm animate-pulse";

                    healthText.className = "font-mono text-rose-500 font-bold";

                } else if (percent <= 50) {

                    healthBar.className = "bg-gradient-to-r from-amber-500 to-yellow-400 h-full rounded-full transition-all duration-200 shadow-sm";

                    healthText.className = "font-mono text-amber-400 font-bold";

                }

            }


            if (playerHealth <= 0) {

                gameOver = true;

                const modal = document.getElementById('gameOverModal');

                if (modal) modal.classList.remove('hidden');

            }

        }


        function checkCollisions(x, z) {

            const playerBox = new THREE.Box3().setFromCenterAndSize(

                new THREE.Vector3(x, 1, z),

                new THREE.Vector3(0.6, 2, 0.6)

            );


            for (let i = 0; i < wallColliders.length; i++) {

                if (wallColliders[i].intersectsBox(playerBox)) {

                    return true;

                }

            }

            return false;

        }


        function updateCharacter(delta) {

            if (gameOver || gameWon) return;


            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 isMoving = moveX !== 0 || moveZ !== 0;


            if (isMoving) {

                const moveDir = new THREE.Vector3(moveX, 0, moveZ).normalize();

                const targetAngle = Math.atan2(moveDir.x, moveDir.z);


                characterState.rotation = targetAngle;

                monigoteGroup.rotation.y = characterState.rotation;


                const step = characterState.speed * delta;

                const nextX = characterState.position.x + moveDir.x * step;

                const nextZ = characterState.position.z + moveDir.z * step;


                if (!checkCollisions(nextX, characterState.position.z)) {

                    characterState.position.x = nextX;

                }

                if (!checkCollisions(characterState.position.x, nextZ)) {

                    characterState.position.z = nextZ;

                }


                monigoteGroup.position.copy(characterState.position);


                characterState.walkCycle += delta * characterState.speed * 2.5;

                const swing = Math.sin(characterState.walkCycle) * 0.5;


                if (limbs.armL) limbs.armL.rotation.x = swing;

                if (limbs.armR) limbs.armR.rotation.x = -swing;

                if (limbs.legL) limbs.legL.rotation.x = -swing;

                if (limbs.legR) limbs.legR.rotation.x = swing;

            } else {

                if (limbs.armL) limbs.armL.rotation.x = 0;

                if (limbs.armR) limbs.armR.rotation.x = 0;

                if (limbs.legL) limbs.legL.rotation.x = 0;

                if (limbs.legR) limbs.legR.rotation.x = 0;

            }

        }


        function updateEnemies(delta) {

            for (let i = enemies.length - 1; i >= 0; i--) {

                const enemy = enemies[i];


                if (enemy.shrinking) {

                    enemy.scale -= delta * 1.5;

                    if (enemy.scale <= 0) {

                        scene.remove(enemy.group);

                        enemies.splice(i, 1);

                        continue;

                    }

                    enemy.group.scale.setScalar(enemy.scale);

                    continue;

                }


                if (gameOver || gameWon) continue;


                // Disparo de enemigos si están cerca del jugador

                enemy.shootCooldown -= delta;

                const distToPlayer = enemy.position.distanceTo(characterState.position);

                if (distToPlayer < 20 && enemy.shootCooldown <= 0) {

                    enemyShoot(enemy);

                    enemy.shootCooldown = 2.0 + Math.random() * 2.0;

                }


                const step = globalEnemySpeed * delta;

                const nextX = enemy.position.x + enemy.direction.x * step;

                const nextZ = enemy.position.z + enemy.direction.z * step;


                if (checkCollisions(nextX, nextZ)) {

                    enemy.rotation += Math.PI * (0.5 + Math.random() * 0.5);

                    enemy.direction.set(Math.sin(enemy.rotation), 0, Math.cos(enemy.rotation)).normalize();

                } else {

                    enemy.position.x = nextX;

                    enemy.position.z = nextZ;

                    enemy.group.position.copy(enemy.position);

                    enemy.group.rotation.y = enemy.rotation;

                }


                if (globalEnemySpeed > 0) {

                    enemy.walkCycle += delta * globalEnemySpeed * 2.5;

                    const swing = Math.sin(enemy.walkCycle) * 0.5;

                    if (enemy.limbs.armL) enemy.limbs.armL.rotation.x = swing;

                    if (enemy.limbs.armR) enemy.limbs.armR.rotation.x = -swing;

                    if (enemy.limbs.legL) enemy.limbs.legL.rotation.x = -swing;

                    if (enemy.limbs.legR) enemy.limbs.legR.rotation.x = swing;

                }

            }

        }


        function updateDoors(delta) {

            doors.forEach(door => {

                const dist = characterState.position.distanceTo(door.position);

                const targetOpen = dist < 5 ? 1 : 0;


                door.openFactor += (targetOpen - door.openFactor) * delta * 4;


                const offset = door.openFactor * (door.width / 2.2);

                if (door.isVertical) {

                    door.left.position.z = -door.width / 4 - offset;

                    door.right.position.z = door.width / 4 + offset;

                } else {

                    door.left.position.x = -door.width / 4 - offset;

                    door.right.position.x = door.width / 4 + offset;

                }

            });

        }


        function updateScoreUI() {

            const badge = document.getElementById('scoreBadge');

            if (badge) badge.textContent = `${score} / ${TARGET_SCORE}`;


            if (score >= TARGET_SCORE && !gameWon && !gameOver) {

                gameWon = true;

                const modal = document.getElementById('victoryModal');

                if (modal) modal.classList.remove('hidden');

            }

        }


        function updateCamera() {

            const camOffset = new THREE.Vector3(0, 14, 16);

            camera.position.copy(characterState.position).add(camOffset);

            camera.lookAt(characterState.position.x, characterState.position.y + 1, characterState.position.z);


            spotlight.position.copy(camera.position);

            spotlight.target.position.copy(characterState.position);

            spotlight.target.updateMatrixWorld();

        }


        function onWindowResize() {

            camera.aspect = window.innerWidth / window.innerHeight;

            camera.updateProjectionMatrix();

            renderer.setSize(window.innerWidth, window.innerHeight);

        }


        let lastTime = performance.now();

        function animate() {

            requestAnimationFrame(animate);


            const now = performance.now();

            const delta = Math.min((now - lastTime) / 1000, 0.1);

            lastTime = now;


            updateCharacter(delta);

            updateEnemies(delta);

            updateProjectiles(delta);

            updateDoors(delta);

            updateCamera();


            renderer.render(scene, camera);

        }


        window.onload = function() {

            initScene();

            animate();

        };

    </script>

</body>

</html>

No hay comentarios:

Publicar un comentario