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>