lunes, 9 de noviembre de 2026

juegos para telefonos;

https://sites.google.com/view/juego-para-movil-y-mu/full-page-embed 

este se puede jugar desde el telefono

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>

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>