Feat: Interactive doors with physical wall cutouts
- Implemented door selection and interaction model (walk-to + click). - Added modal for opening doors. - Refactored wall rendering to create physical holes (CSG-like wall segments). - Aligned door meshes to perfectly fit wall cutouts. - Managed door visibility states to prevent Z-fighting on open doors.
This commit is contained in:
19
index.html
19
index.html
@@ -12,13 +12,22 @@
|
||||
<div id="app"></div>
|
||||
<div id="hud">
|
||||
<div id="minimap-container">
|
||||
<canvas id="minimap" width="200" height="200"></canvas>
|
||||
<canvas id="minimap"></canvas>
|
||||
</div>
|
||||
<div id="compass">
|
||||
<div id="compass-n" class="compass-btn active" data-direction="N">N</div>
|
||||
<div id="compass-s" class="compass-btn" data-direction="S">S</div>
|
||||
<div id="compass-e" class="compass-btn" data-direction="E">E</div>
|
||||
<div id="compass-w" class="compass-btn" data-direction="W">W</div>
|
||||
<div class="compass-btn" data-dir="N">N</div>
|
||||
<div class="compass-row">
|
||||
<div class="compass-btn" data-dir="W">W</div>
|
||||
<div class="compass-btn" data-dir="E">E</div>
|
||||
</div>
|
||||
<div class="compass-btn" data-dir="S">S</div>
|
||||
</div>
|
||||
<div id="door-modal" class="hidden">
|
||||
<div class="modal-content">
|
||||
<p>¿Quieres abrir la puerta?</p>
|
||||
<button id="btn-open-yes">Sí</button>
|
||||
<button id="btn-open-no">No</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
|
||||
368
src/main.js
368
src/main.js
@@ -206,19 +206,14 @@ function generateDungeon() {
|
||||
// E/W: Alineados en Y -> puerta en Y relativo es igual para ambos.
|
||||
|
||||
const doorConfig = dir.side === 'N' || dir.side === 'S'
|
||||
? { side: dir.side, gridX: doorGridPos, leadsTo: newRoomId }
|
||||
: { side: dir.side, gridY: doorGridPos, leadsTo: newRoomId };
|
||||
? { side: dir.side, gridX: doorGridPos, leadsTo: newRoomId, isOpen: false, id: `door_${currentRoom.id}_to_${newRoomId}` }
|
||||
: { side: dir.side, gridY: doorGridPos, leadsTo: newRoomId, isOpen: false, id: `door_${currentRoom.id}_to_${newRoomId}` };
|
||||
|
||||
currentRoom.doors.push(doorConfig);
|
||||
|
||||
// Puerta en la sala nueva (destino)
|
||||
// Necesitamos calcular la posición relativa correcta.
|
||||
// Al estar alineados top/left, el offset relativo es el mismo (doorGridPos).
|
||||
// (Si hubieramos centrado las salas, esto sería más complejo)
|
||||
|
||||
const oppositeDoorConfig = dir.opposite === 'N' || dir.opposite === 'S'
|
||||
? { side: dir.opposite, gridX: doorGridPos, leadsTo: currentRoom.id }
|
||||
: { side: dir.opposite, gridY: doorGridPos, leadsTo: currentRoom.id };
|
||||
? { side: dir.opposite, gridX: doorGridPos, leadsTo: currentRoom.id, isOpen: false, id: `door_${newRoomId}_to_${currentRoom.id}` }
|
||||
: { side: dir.opposite, gridY: doorGridPos, leadsTo: currentRoom.id, isOpen: false, id: `door_${newRoomId}_to_${currentRoom.id}` };
|
||||
|
||||
newRoom.doors.push(oppositeDoorConfig);
|
||||
}
|
||||
@@ -241,12 +236,13 @@ const ROOMS = generateDungeon();
|
||||
|
||||
const SESSION = {
|
||||
selectedUnitId: null,
|
||||
path: [], // Array de {x, y}
|
||||
pathMeshes: [], // Array de meshes visuales
|
||||
roomMeshes: {}, // { roomId: { tile: mesh, walls: [], doors: [], entities: [] } }
|
||||
selectedDoorId: null, // Nuevo: ID de la puerta seleccionada
|
||||
path: [],
|
||||
pathMeshes: [],
|
||||
roomMeshes: {},
|
||||
isAnimating: false,
|
||||
textureCache: {}, // Cache de texturas cargadas
|
||||
currentView: 'N' // Vista actual: N, S, E, W
|
||||
textureCache: {},
|
||||
currentView: 'N'
|
||||
};
|
||||
|
||||
// --- CONFIGURACIÓN BÁSICA THREE.JS ---
|
||||
@@ -489,24 +485,25 @@ function isPositionDoor(x, y, room) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verificar si una celda es transitable
|
||||
|
||||
// Verificar si una celda es transitable (bloquear puertas cerradas)
|
||||
function isWalkable(x, y) {
|
||||
// Verificar en todas las salas visitadas
|
||||
for (const roomId of ROOMS.visitedRooms) {
|
||||
const room = ROOMS.rooms.find(r => r.id === roomId);
|
||||
if (!room) continue;
|
||||
|
||||
// Si está dentro de la sala, es transitable
|
||||
if (isPositionInRoom(x, y, room)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Si es una puerta de la sala, es transitable
|
||||
if (isPositionDoor(x, y, room)) {
|
||||
return true;
|
||||
// Verificar puertas
|
||||
for (const door of room.doors) {
|
||||
const doorPos = getDoorGridPosition(room, door);
|
||||
if (doorPos.x === x && doorPos.y === y) {
|
||||
return door.isOpen; // Solo transitable si está abierta
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -562,7 +559,7 @@ function updatePathVisuals() {
|
||||
|
||||
// --- MANEJO VISUAL DE SELECCIÓN ---
|
||||
function updateSelectionVisuals() {
|
||||
// Buscar en todas las salas visitadas
|
||||
// Unidades
|
||||
ROOMS.visitedRooms.forEach(roomId => {
|
||||
const room = ROOMS.rooms.find(r => r.id === roomId);
|
||||
if (!room) return;
|
||||
@@ -580,6 +577,132 @@ function updateSelectionVisuals() {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Puertas
|
||||
Object.keys(SESSION.roomMeshes).forEach(roomId => {
|
||||
const roomData = SESSION.roomMeshes[roomId];
|
||||
if (roomData.doors) {
|
||||
roomData.doors.forEach(doorMesh => {
|
||||
// Asumimos que guardamos el ID de la puerta en userData al crear el mesh
|
||||
if (doorMesh.userData.id === SESSION.selectedDoorId) {
|
||||
doorMesh.material.color.setHex(0xffff00);
|
||||
doorMesh.material.opacity = 0.5;
|
||||
doorMesh.material.transparent = true;
|
||||
} else {
|
||||
doorMesh.material.color.setHex(0xffffff);
|
||||
// Restaurar opacidad original (si era transparente) o 1.0
|
||||
// Por simplicidad, puertas cerradas opacas, abiertas transparentes?
|
||||
// No, el modal decide. Dejamos como estaba por defecto.
|
||||
doorMesh.material.opacity = 1.0;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- LOGICA MODAL PUERTAS ---
|
||||
const modal = document.getElementById('door-modal');
|
||||
const btnYes = document.getElementById('btn-open-yes');
|
||||
const btnNo = document.getElementById('btn-open-no');
|
||||
|
||||
btnYes.addEventListener('click', confirmOpenDoor);
|
||||
btnNo.addEventListener('click', closeDoorModal);
|
||||
|
||||
function openDoorModal() {
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeDoorModal() {
|
||||
modal.classList.add('hidden');
|
||||
// Deseleccionar si cancela
|
||||
if (SESSION.selectedDoorId) {
|
||||
SESSION.selectedDoorId = null;
|
||||
updateSelectionVisuals();
|
||||
}
|
||||
}
|
||||
|
||||
function confirmOpenDoor() {
|
||||
if (!SESSION.selectedDoorId) return;
|
||||
|
||||
// Buscar la puerta
|
||||
let targetDoor = null;
|
||||
let originRoom = null;
|
||||
|
||||
for (const room of ROOMS.rooms) {
|
||||
const found = room.doors.find(d => d.id === SESSION.selectedDoorId);
|
||||
if (found) {
|
||||
targetDoor = found;
|
||||
originRoom = room;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetDoor && originRoom) {
|
||||
console.log("Abriendo puerta:", targetDoor.id);
|
||||
targetDoor.isOpen = true;
|
||||
|
||||
// Abrir también la puerta inversa (la de la otra sala)
|
||||
const targetRoom = ROOMS.rooms.find(r => r.id === targetDoor.leadsTo);
|
||||
if (targetRoom) {
|
||||
const oppositeDoor = targetRoom.doors.find(d => d.leadsTo === originRoom.id);
|
||||
if (oppositeDoor) {
|
||||
oppositeDoor.isOpen = true;
|
||||
|
||||
// Si la sala destino YA está renderizada, ocultar visualmente su puerta también
|
||||
if (SESSION.roomMeshes[targetRoom.id]) {
|
||||
const oppDoorMesh = SESSION.roomMeshes[targetRoom.id].doors.find(m => m.userData.id === oppositeDoor.id);
|
||||
if (oppDoorMesh) {
|
||||
oppDoorMesh.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Revelar sala destino
|
||||
if (!ROOMS.visitedRooms.has(targetRoom.id)) {
|
||||
ROOMS.visitedRooms.add(targetRoom.id);
|
||||
renderRoom(targetRoom);
|
||||
}
|
||||
}
|
||||
|
||||
// Actualizar visual del mesh (hacerla invisible o rotarla)
|
||||
// Buscamos el mesh en roomMeshes
|
||||
if (SESSION.roomMeshes[originRoom.id]) {
|
||||
const doorMesh = SESSION.roomMeshes[originRoom.id].doors.find(m => m.userData.id === targetDoor.id);
|
||||
if (doorMesh) {
|
||||
doorMesh.visible = false; // "Abrir" visualmente desapareciendo
|
||||
}
|
||||
}
|
||||
|
||||
// Limipiar selección y cerrar modal
|
||||
SESSION.selectedDoorId = null;
|
||||
updateSelectionVisuals();
|
||||
closeDoorModal();
|
||||
drawMinimap();
|
||||
}
|
||||
}
|
||||
|
||||
function checkDoorInteraction(unit) {
|
||||
if (!SESSION.selectedDoorId) return;
|
||||
|
||||
// Buscar puerta seleccionada
|
||||
let targetDoor = null;
|
||||
let room = null;
|
||||
for (const r of ROOMS.rooms) {
|
||||
targetDoor = r.doors.find(d => d.id === SESSION.selectedDoorId);
|
||||
if (targetDoor) {
|
||||
room = r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetDoor && !targetDoor.isOpen) {
|
||||
const doorPos = getDoorGridPosition(room, targetDoor);
|
||||
|
||||
// Verificar adyacencia
|
||||
if (isAdjacent({ x: unit.x, y: unit.y }, doorPos)) {
|
||||
openDoorModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- ANIMACIÓN DE MOVIMIENTO ---
|
||||
@@ -612,7 +735,7 @@ async function animateMovement() {
|
||||
const targetWorldPos = gridToWorld(targetGridPos.x, targetGridPos.y);
|
||||
const endPos = { x: targetWorldPos.x, z: targetWorldPos.z };
|
||||
|
||||
const duration = 300;
|
||||
const duration = 200;
|
||||
const startTime = Date.now();
|
||||
const standeeHeight = ASSETS.standees[unit.type].height;
|
||||
|
||||
@@ -627,7 +750,8 @@ async function animateMovement() {
|
||||
unit.mesh.position.x = startPos.x + (endPos.x - startPos.x) * eased;
|
||||
unit.mesh.position.z = startPos.z + (endPos.z - startPos.z) * eased;
|
||||
|
||||
const hopHeight = 0.8;
|
||||
// Salto visual más sutil
|
||||
const hopHeight = 0.5;
|
||||
const hopProgress = Math.sin(progress * Math.PI);
|
||||
unit.mesh.position.y = (standeeHeight / 2) + (hopProgress * hopHeight);
|
||||
|
||||
@@ -653,10 +777,10 @@ async function animateMovement() {
|
||||
unit.x = step.x;
|
||||
unit.y = step.y;
|
||||
|
||||
// 1. Verificar si hemos pisado una puerta (Para renderizar lo siguiente antes de entrar)
|
||||
checkDoorTransition(unit, unitRoom);
|
||||
// YA NO USAMOS checkDoorTransition automática para revelar/teletransportar
|
||||
// en su lugar usamos la lógica de puertas interactivas
|
||||
|
||||
// 2. AUTO-CORRECCIÓN: Verificar en qué sala estamos FÍSICAMENTE
|
||||
// 2. AUTO-CORRECCIÓN: Seguir usándola por seguridad si entramos
|
||||
const actualRoom = detectRoomChange(unit, unitRoom);
|
||||
if (actualRoom) {
|
||||
unitRoom = actualRoom;
|
||||
@@ -666,6 +790,9 @@ async function animateMovement() {
|
||||
updatePathVisuals();
|
||||
}
|
||||
|
||||
// Al terminar movimiento, verificar interacción con puerta
|
||||
checkDoorInteraction(unit);
|
||||
|
||||
// Centrar cámara en el personaje manteniendo el offset de la vista actual
|
||||
const newTarget = unit.mesh.position.clone();
|
||||
newTarget.y = 0;
|
||||
@@ -675,7 +802,7 @@ async function animateMovement() {
|
||||
controls.target.copy(newTarget);
|
||||
camera.position.copy(newTarget).add(currentOffset);
|
||||
|
||||
SESSION.selectedUnitId = null;
|
||||
SESSION.selectedUnitId = null; // Deseleccionar unidad al terminar de mover
|
||||
updateSelectionVisuals();
|
||||
SESSION.isAnimating = false;
|
||||
drawMinimap(); // Actualizar posición final del jugador
|
||||
@@ -848,37 +975,127 @@ async function renderRoom(room) {
|
||||
{ side: 'W', width: worldHeight, offset: { x: -halfSizeX, z: 0 }, rotation: Math.PI / 2 }
|
||||
];
|
||||
|
||||
// Calcular posiciones de puertas para procesar paredes
|
||||
// Mapa: Side -> Door (solo soportamos 1 puerta por pared por ahora para simplificar)
|
||||
const doorsOnSides = {};
|
||||
room.doors.forEach(d => { doorsOnSides[d.side] = d; });
|
||||
|
||||
for (const config of wallConfigs) {
|
||||
if (room.walls.includes(config.side)) {
|
||||
const opacity = getWallOpacity(config.side, SESSION.currentView);
|
||||
const wallSide = config.side;
|
||||
const door = doorsOnSides[wallSide];
|
||||
|
||||
// Textura adaptada al ancho específico de esta pared
|
||||
const materialTex = wallTex.clone();
|
||||
// Ajustar repetición horizontal según longitud de la pared (aprox 1 repetición cada 2 celdas grandes)
|
||||
materialTex.repeat.set(config.width / (CONFIG.CELL_SIZE * 2), 2);
|
||||
// Función helper para crear un segmento de pared
|
||||
const createWallSegment = (w, h, xOffset, yOffset, opacity, name) => {
|
||||
if (w <= 0.01) return; // Evitar segmentos degenerados
|
||||
|
||||
const wallMaterial = new THREE.MeshStandardMaterial({
|
||||
map: materialTex,
|
||||
const segmentGeometry = new THREE.PlaneGeometry(w, h);
|
||||
|
||||
// Ajustar textura al tamaño del segmento
|
||||
const segmentTex = wallTex.clone();
|
||||
segmentTex.wrapS = THREE.RepeatWrapping;
|
||||
segmentTex.wrapT = THREE.RepeatWrapping;
|
||||
segmentTex.repeat.set(w / 2, h / (wallHeight / 2)); // Mantener densidad aprox
|
||||
|
||||
const segmentMaterial = new THREE.MeshStandardMaterial({
|
||||
map: segmentTex,
|
||||
transparent: opacity < 1.0,
|
||||
opacity: opacity,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
// Geometría específica para el ancho de ESTA pared
|
||||
const wallGeometry = new THREE.PlaneGeometry(config.width, wallHeight);
|
||||
const wall = new THREE.Mesh(segmentGeometry, segmentMaterial);
|
||||
|
||||
// Calculamos posición RELATIVA al centro de la pared "ideal"
|
||||
// La pared ideal está en config.offset
|
||||
// Rotamos el offset local del segmento según la rotación de la pared
|
||||
|
||||
const localX = xOffset;
|
||||
const localZ = 0; // En el plano de la pared
|
||||
|
||||
// Rotar vector (localX, 0) por config.rotation
|
||||
// Plane geometry is created at origin. We rotate it around Y.
|
||||
// A segment meant to be at "xOffset" along the plane's width needs to be translated.
|
||||
|
||||
// Posición de la pared "Base"
|
||||
const baseX = centerX + config.offset.x;
|
||||
const baseZ = centerZ + config.offset.z;
|
||||
|
||||
// Vector dirección de la pared (Hacia la derecha de la pared)
|
||||
// PlaneGeometry +X is "Right"
|
||||
const dirX = Math.cos(config.rotation);
|
||||
const dirZ = -Math.sin(config.rotation);
|
||||
|
||||
wall.position.x = baseX + (dirX * xOffset);
|
||||
wall.position.z = baseZ + (dirZ * xOffset);
|
||||
wall.position.y = yOffset; // Altura absoluta
|
||||
|
||||
const wall = new THREE.Mesh(wallGeometry, wallMaterial);
|
||||
wall.position.set(
|
||||
centerX + config.offset.x,
|
||||
wallHeight / 2,
|
||||
centerZ + config.offset.z
|
||||
);
|
||||
wall.rotation.y = config.rotation;
|
||||
wall.castShadow = true;
|
||||
wall.receiveShadow = true;
|
||||
wall.userData.wallSide = config.side; // Metadata para identificar el lado
|
||||
wall.userData.wallSide = config.side;
|
||||
scene.add(wall);
|
||||
roomMeshes.walls.push(wall);
|
||||
};
|
||||
|
||||
const opacity = getWallOpacity(config.side, SESSION.currentView); // Se actualiza dinámicamente
|
||||
|
||||
if (!door) {
|
||||
// PARED SOLIDA (Caso original simplificado)
|
||||
createWallSegment(config.width, wallHeight, 0, wallHeight / 2, opacity, "FullWall");
|
||||
} else {
|
||||
// PARED CON HUECO
|
||||
const doorWidth = 1.5;
|
||||
const doorHeight = 2.0;
|
||||
|
||||
// Calcular posición relativa de la puerta en la pared
|
||||
// config.width es el ancho total. El rango local es [-W/2, W/2]
|
||||
|
||||
// Obtener coordenadas de la puerta
|
||||
const doorGridPos = getDoorGridPosition(room, door);
|
||||
const doorWorldPos = gridToWorld(doorGridPos.x, doorGridPos.y);
|
||||
|
||||
// Necesitamos proyectar la posición de la puerta sobre el eje de la pared para saber su offset
|
||||
// Pared N/S: Offset es diferencia en X.
|
||||
// Pared E/W: Offset es diferencia en Z (pero ojo con la dirección del plano).
|
||||
|
||||
let doorOffset = 0; // Offset del centro de la puerta respecto al centro de la pared
|
||||
if (config.side === 'N') {
|
||||
doorOffset = doorWorldPos.x - centerX;
|
||||
} else if (config.side === 'S') {
|
||||
doorOffset = -(doorWorldPos.x - centerX); // S wall is rotated 180, local X is opposite world X
|
||||
} else if (config.side === 'E') {
|
||||
doorOffset = doorWorldPos.z - centerZ;
|
||||
} else if (config.side === 'W') {
|
||||
doorOffset = -(doorWorldPos.z - centerZ); // W wall is rotated 90, local X is opposite world Z
|
||||
}
|
||||
|
||||
const w = config.width;
|
||||
|
||||
// Segmento Izquierdo: Desde -w/2 hasta (doorOffset - doorWidth/2)
|
||||
const leftEnd = doorOffset - (doorWidth / 2);
|
||||
const leftStart = -w / 2;
|
||||
const leftWidth = leftEnd - leftStart;
|
||||
const leftCenter = leftStart + (leftWidth / 2);
|
||||
|
||||
createWallSegment(leftWidth, wallHeight, leftCenter, wallHeight / 2, opacity, "LeftSeg");
|
||||
|
||||
// Segmento Derecho: Desde (doorOffset + doorWidth/2) hasta w/2
|
||||
const rightStart = doorOffset + (doorWidth / 2);
|
||||
const rightEnd = w / 2;
|
||||
const rightWidth = rightEnd - rightStart;
|
||||
const rightCenter = rightStart + (rightWidth / 2);
|
||||
|
||||
createWallSegment(rightWidth, wallHeight, rightCenter, wallHeight / 2, opacity, "RightSeg");
|
||||
|
||||
// Dintel (Arriba de la puerta)
|
||||
// Ancho: doorWidth
|
||||
// Altura: wallHeight - doorHeight (2.5 - 2.0 = 0.5)
|
||||
// Centro X: doorOffset
|
||||
// Centro Y: doorHeight + (dintelHeight / 2) -> 2.0 + 0.25 = 2.25
|
||||
const lintelHeight = wallHeight - doorHeight;
|
||||
if (lintelHeight > 0) {
|
||||
createWallSegment(doorWidth, lintelHeight, doorOffset, doorHeight + (lintelHeight / 2), opacity, "Lintel");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -904,24 +1121,30 @@ async function renderRoom(room) {
|
||||
});
|
||||
|
||||
const doorMesh = new THREE.Mesh(doorGeometry, doorMaterial);
|
||||
doorMesh.userData.id = door.id;
|
||||
doorMesh.visible = !door.isOpen; // Ocultar si ya está abierta
|
||||
const doorGridPos = getDoorGridPosition(room, door);
|
||||
const doorWorldPos = gridToWorld(doorGridPos.x, doorGridPos.y);
|
||||
|
||||
switch (door.side) {
|
||||
case 'N':
|
||||
doorMesh.position.set(doorWorldPos.x, doorHeight / 2, centerZ - halfSizeZ + 0.05);
|
||||
// Pared Norte: Alinear X con worldPos, Z con borde norte
|
||||
doorMesh.position.set(doorWorldPos.x, doorHeight / 2, centerZ - halfSizeZ);
|
||||
doorMesh.rotation.y = 0;
|
||||
break;
|
||||
case 'S':
|
||||
doorMesh.position.set(doorWorldPos.x, doorHeight / 2, centerZ + halfSizeZ - 0.05);
|
||||
doorMesh.rotation.y = 0;
|
||||
// Pared Sur
|
||||
doorMesh.position.set(doorWorldPos.x, doorHeight / 2, centerZ + halfSizeZ);
|
||||
doorMesh.rotation.y = 0; // O Math.PI, visualmente igual para puerta plana 1.5x2
|
||||
break;
|
||||
case 'E':
|
||||
doorMesh.position.set(centerX + halfSizeX - 0.05, doorHeight / 2, doorWorldPos.z);
|
||||
// Pared Este: Alinear Z con worldPos, X con borde este
|
||||
doorMesh.position.set(centerX + halfSizeX, doorHeight / 2, doorWorldPos.z);
|
||||
doorMesh.rotation.y = Math.PI / 2;
|
||||
break;
|
||||
case 'W':
|
||||
doorMesh.position.set(centerX - halfSizeX + 0.05, doorHeight / 2, doorWorldPos.z);
|
||||
// Pared Oeste
|
||||
doorMesh.position.set(centerX - halfSizeX, doorHeight / 2, doorWorldPos.z);
|
||||
doorMesh.rotation.y = Math.PI / 2;
|
||||
break;
|
||||
}
|
||||
@@ -984,7 +1207,7 @@ function updateCompassUI() {
|
||||
document.querySelectorAll('.compass-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
const activeBtn = document.querySelector(`[data-direction="${SESSION.currentView}"]`);
|
||||
const activeBtn = document.querySelector(`[data-dir="${SESSION.currentView}"]`);
|
||||
if (activeBtn) {
|
||||
activeBtn.classList.add('active');
|
||||
}
|
||||
@@ -1090,12 +1313,14 @@ function drawMinimap() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Event listeners para los botones del compás
|
||||
document.querySelectorAll('.compass-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const direction = btn.getAttribute('data-direction');
|
||||
setCameraView(direction);
|
||||
const direction = btn.getAttribute('data-dir');
|
||||
if (direction) {
|
||||
setCameraView(direction);
|
||||
updateCompassUI();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1133,6 +1358,7 @@ window.addEventListener('pointerdown', (event) => {
|
||||
if (entity) {
|
||||
console.log("Seleccionado:", entity.type);
|
||||
SESSION.selectedUnitId = entity.id;
|
||||
SESSION.selectedDoorId = null; // Deseleccionar puerta
|
||||
SESSION.path = [];
|
||||
updatePathVisuals();
|
||||
updateSelectionVisuals();
|
||||
@@ -1140,6 +1366,40 @@ window.addEventListener('pointerdown', (event) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Detectar click en puertas
|
||||
const allDoors = [];
|
||||
Object.values(SESSION.roomMeshes).forEach(roomData => {
|
||||
if (roomData.doors) {
|
||||
allDoors.push(...roomData.doors);
|
||||
}
|
||||
});
|
||||
|
||||
const intersectsDoors = raycaster.intersectObjects(allDoors);
|
||||
if (intersectsDoors.length > 0) {
|
||||
const clickedDoor = intersectsDoors[0].object;
|
||||
if (clickedDoor.userData.id) {
|
||||
console.log("Puerta seleccionada:", clickedDoor.userData.id);
|
||||
SESSION.selectedDoorId = clickedDoor.userData.id;
|
||||
SESSION.selectedUnitId = null;
|
||||
SESSION.path = [];
|
||||
updatePathVisuals();
|
||||
updateSelectionVisuals();
|
||||
|
||||
// Verificar interacción inmediata (si ya estamos al lado)
|
||||
// Buscamos al héroe principal (asumimos que es el que controlamos)
|
||||
let hero = null;
|
||||
for (const r of ROOMS.rooms) {
|
||||
hero = r.entities.find(e => e.type === 'hero_1');
|
||||
if (hero) break;
|
||||
}
|
||||
|
||||
if (hero) {
|
||||
checkDoorInteraction(hero);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Procesar click en suelo
|
||||
if (SESSION.selectedUnitId) {
|
||||
const intersectsGround = raycaster.intersectObject(raycastPlane);
|
||||
|
||||
@@ -121,4 +121,66 @@ canvas {
|
||||
#compass-w {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
}
|
||||
/* Modal Styles */
|
||||
#door-modal {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 2000;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
#door-modal.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #2a2a2a;
|
||||
padding: 20px;
|
||||
border: 2px solid #555;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.modal-content p {
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.modal-content button {
|
||||
padding: 8px 20px;
|
||||
margin: 0 10px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#btn-open-yes {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#btn-open-yes:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
#btn-open-no {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#btn-open-no:hover {
|
||||
background: #d32f2f;
|
||||
}
|
||||
|
||||
|
||||
124
src/style.css.bak
Normal file
124
src/style.css.bak
Normal file
@@ -0,0 +1,124 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
color-scheme: light dark;
|
||||
background-color: #242424;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
/* Evitar scrollbars por el canvas */
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* HUD Wrapper */
|
||||
#hud {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
/* Dejar pasar clics al juego 3D */
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* UI Elements inside HUD (reactivate pointer events) */
|
||||
#hud>* {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Minimap */
|
||||
#minimap-container {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border: 2px solid #444;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#minimap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Compass UI */
|
||||
#compass {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr 1fr;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.compass-btn {
|
||||
background: rgba(50, 50, 50, 0.8);
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.compass-btn:hover {
|
||||
background: rgba(70, 70, 70, 0.9);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.compass-btn.active {
|
||||
background: rgba(255, 200, 0, 0.9);
|
||||
border-color: rgba(255, 220, 0, 1);
|
||||
color: rgba(0, 0, 0, 1);
|
||||
box-shadow: 0 0 15px rgba(255, 200, 0, 0.6);
|
||||
}
|
||||
|
||||
#compass-n {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
#compass-s {
|
||||
grid-column: 2;
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
#compass-e {
|
||||
grid-column: 3;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
#compass-w {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
Reference in New Issue
Block a user