https://sites.google.com/view/juego3dmonigoteenpasilloscilnd/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 gap-2 text-amber-400">
<span>🚶</span> Monigote 3D
</h1>
<p class="text-xs text-slate-300 mb-4 leading-relaxed">
Explora los pasillos y habitaciones. Las puertas se abrirán automáticamente cuando te acerques a ellas.
</p>
<!-- Controles de Teclado -->
<div class="mb-4 bg-slate-800/60 p-3 rounded-lg border border-slate-700/40">
<span class="text-xs font-semibold text-slate-400 uppercase tracking-wider block mb-2">Controles (WASD)</span>
<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>
<!-- Slider de Velocidad -->
<div class="space-y-1">
<div class="flex justify-between items-center text-xs">
<label for="speedSlider" class="font-medium text-slate-300">Calibrar Velocidad</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-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-amber-400">
</div>
<!-- Indicador de Puertas -->
<div id="doorAlert" class="mt-3 py-1.5 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>
<!-- Script del Juego -->
<script>
// Variables principales Three.js
let scene, camera, renderer;
let monigoteGroup, limbs = {};
let spotlight, streetlightGroup;
// Arreglo de colisiones físicas (Paredes) y Puertas activas
const wallColliders = [];
const doors = [];
// 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 };
window.onload = function() {
initScene();
buildMap();
createStreetlight();
createMonigote();
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
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);
}
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');
}
});
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');
}
});
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);
});
}
}
// Gestión del movimiento, animación y colisión de paredes
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;
// Intento de movimiento por ejes por separado para evitar atascos en paredes (Sliding)
const nextPosX = characterState.position.x + moveDir.x * stepDistance;
const nextPosZ = characterState.position.z + moveDir.z * stepDistance;
// 1. Probar movimiento en Eje X
if (!checkCollisions(nextPosX, characterState.position.z)) {
characterState.position.x = nextPosX;
}
// 2. Probar movimiento en Eje Z
if (!checkCollisions(characterState.position.x, nextPosZ)) {
characterState.position.z = nextPosZ;
}
// Girar personaje en la dirección del movimiento
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;
// Animación de Brazos y Piernas
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;
}
// 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) { // Si no está suficientemente abierta, actúa como pared
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);
// Si el personaje está dentro del radio, abrir la puerta
if (dist < door.triggerRadius) {
door.targetOpen = 1;
doorOpeningNear = true;
} else {
door.targetOpen = 0;
}
// Interpolar suavemente el estado de la puerta
door.currentOpen += (door.targetOpen - door.currentOpen) * (delta * 5);
// Elevación de la puerta al abrirse
door.mesh.position.y = (door.height / 2) + (door.currentOpen * (door.height - 0.2));
updateDoorCollider(door);
});
// Mostrar u ocultar alerta UI de puertas
const doorAlertEl = document.getElementById('doorAlert');
if (doorAlertEl) {
doorAlertEl.style.opacity = doorOpeningNear ? '1' : '0';
}
}
// Actualización de Cámara y Farola (Visibilidad constante sin traspasar)
function updateCameraAndStreetlight() {
// Posicionar la farola por encima del muñeco con un offset continuo
const targetPos = monigoteGroup.position.clone();
// La farola y su luz siguen al monigote desde arriba
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();
// Posición de la cámara desde el foco de la farola mirando hacia abajo al personaje
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);
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>
