2 Commits

Author SHA1 Message Date
7cc92da012 Mejoras en vistas isométricas y sistema de opacidad de paredes
- Sistema de opacidad dinámica de paredes según vista actual
- Vistas con transición animada suave (600ms)
- Centrado automático en el jugador al cambiar vista
- Quaternions precalculados para evitar degradación de vistas
- Validación de puertas que apuntan a salas existentes
- Limpieza de puertas inválidas en generador de mazmorras
- Paredes opacas/transparentes según orientación de cámara

Pendiente: Resolver z-fighting de puertas en ciertas vistas
2025-12-21 00:43:36 +01:00
92fdfed49c Fix: Vistas isométricas con quaternions fijos para evitar degradación
- Precálculo de quaternions para cada vista (N, S, E, W)
- Eliminado uso de lookAt() en cambios de vista
- Uso de Vector3 nativos de Three.js
- Sistema completamente determinista sin acumulación de errores
2025-12-21 00:25:30 +01:00

View File

@@ -126,6 +126,18 @@ function generateDungeon() {
}
}
// Limpiar puertas que apuntan a salas inexistentes
const existingRoomIds = new Set(rooms.map(r => r.id));
rooms.forEach(room => {
room.doors = room.doors.filter(door => {
const isValid = existingRoomIds.has(door.leadsTo);
if (!isValid) {
console.log(`Eliminando puerta inválida en sala ${room.id} que apunta a sala ${door.leadsTo}`);
}
return isValid;
});
});
return {
rooms: rooms,
visitedRooms: new Set([1]),
@@ -157,40 +169,169 @@ document.querySelector('#app').appendChild(renderer.domElement);
// Cámara isométrica (zoom más cercano)
const aspect = window.innerWidth / window.innerHeight;
const d = 8; // Reducido de 10 a 8 para zoom aún más cercano
const d = 8;
const camera = new THREE.OrthographicCamera(-d * aspect, d * aspect, d, -d, 1, 1000);
// Vistas isométricas predefinidas
// Vistas isométricas COMPLETAMENTE predefinidas (sin acumulación de errores)
// Cada vista tiene posición, target Y quaternion fijo
const CAMERA_VIEWS = {
N: { position: { x: 20, y: 20, z: 20 }, target: { x: 0, y: 0, z: 0 } }, // Norte (default)
S: { position: { x: -20, y: 20, z: -20 }, target: { x: 0, y: 0, z: 0 } }, // Sur
E: { position: { x: -20, y: 20, z: 20 }, target: { x: 0, y: 0, z: 0 } }, // Este
W: { position: { x: 20, y: 20, z: -20 }, target: { x: 0, y: 0, z: 0 } } // Oeste
N: {
position: new THREE.Vector3(20, 20, 20),
target: new THREE.Vector3(0, 0, 0),
up: new THREE.Vector3(0, 1, 0)
},
S: {
position: new THREE.Vector3(-20, 20, -20),
target: new THREE.Vector3(0, 0, 0),
up: new THREE.Vector3(0, 1, 0)
},
E: {
position: new THREE.Vector3(-20, 20, 20),
target: new THREE.Vector3(0, 0, 0),
up: new THREE.Vector3(0, 1, 0)
},
W: {
position: new THREE.Vector3(20, 20, -20),
target: new THREE.Vector3(0, 0, 0),
up: new THREE.Vector3(0, 1, 0)
}
};
// Precalcular quaternions para cada vista (FIJOS, nunca cambian)
Object.keys(CAMERA_VIEWS).forEach(key => {
const view = CAMERA_VIEWS[key];
const tempCamera = new THREE.PerspectiveCamera();
tempCamera.position.copy(view.position);
tempCamera.up.copy(view.up);
tempCamera.lookAt(view.target);
view.quaternion = tempCamera.quaternion.clone();
});
// OrbitControls solo para zoom y paneo (sin rotación)
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableRotate = false; // Deshabilitar rotación
controls.enableRotate = false;
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.screenSpacePanning = true;
controls.mouseButtons = {
LEFT: null, // Click izquierdo libre para selección
MIDDLE: THREE.MOUSE.PAN, // Paneo con botón central
RIGHT: THREE.MOUSE.PAN // Paneo también con botón derecho
LEFT: null,
MIDDLE: THREE.MOUSE.PAN,
RIGHT: THREE.MOUSE.PAN
};
controls.zoomToCursor = true;
controls.minZoom = 0.5;
controls.maxZoom = 3;
function setCameraView(direction) {
// Determinar opacidad de pared según vista actual
function getWallOpacity(wallSide, viewDirection) {
const opacityRules = {
N: { opaque: ['N', 'W'], transparent: ['S', 'E'] },
S: { opaque: ['S', 'E'], transparent: ['N', 'W'] },
E: { opaque: ['N', 'E'], transparent: ['S', 'W'] },
W: { opaque: ['W', 'S'], transparent: ['N', 'E'] }
};
const rule = opacityRules[viewDirection];
if (rule.opaque.includes(wallSide)) {
return 1.0; // Opaco
} else {
return 0.5; // Semi-transparente
}
}
// Actualizar opacidades de todas las paredes según la vista actual
function updateWallOpacities() {
Object.values(SESSION.roomMeshes).forEach(roomData => {
if (roomData.walls) {
roomData.walls.forEach(wall => {
const wallSide = wall.userData.wallSide;
if (wallSide) {
const newOpacity = getWallOpacity(wallSide, SESSION.currentView);
wall.material.opacity = newOpacity;
wall.material.transparent = newOpacity < 1.0;
}
});
}
});
}
function setCameraView(direction, animate = true) {
const view = CAMERA_VIEWS[direction];
camera.position.set(view.position.x, view.position.y, view.position.z);
camera.lookAt(view.target.x, view.target.y, view.target.z);
controls.target.set(view.target.x, view.target.y, view.target.z);
controls.update();
// Encontrar el personaje del jugador para centrar la vista
let playerPosition = new THREE.Vector3(0, 0, 0);
for (const room of ROOMS.rooms) {
const player = room.entities.find(e => e.type === 'hero_1');
if (player && player.mesh) {
playerPosition.copy(player.mesh.position);
playerPosition.y = 0;
break;
}
}
// Calcular offset de la vista (diferencia entre posición y target)
const viewOffset = view.position.clone().sub(view.target);
// Nueva posición de cámara centrada en el jugador
const newPosition = playerPosition.clone().add(viewOffset);
const newTarget = playerPosition.clone();
if (animate && SESSION.currentView !== direction) {
// Animación suave de transición
const startPos = camera.position.clone();
const startQuat = camera.quaternion.clone();
const startTarget = controls.target.clone();
const duration = 600; // ms
const startTime = Date.now();
const animateTransition = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
// Easing suave (ease-in-out)
const eased = progress < 0.5
? 2 * progress * progress
: 1 - Math.pow(-2 * progress + 2, 2) / 2;
// Interpolar posición
camera.position.lerpVectors(startPos, newPosition, eased);
// Interpolar quaternion (rotación suave)
camera.quaternion.slerpQuaternions(startQuat, view.quaternion, eased);
// Interpolar target
controls.target.lerpVectors(startTarget, newTarget, eased);
camera.up.copy(view.up);
controls.update();
if (progress < 1) {
requestAnimationFrame(animateTransition);
} else {
// Asegurar valores finales exactos
camera.position.copy(newPosition);
camera.quaternion.copy(view.quaternion);
camera.up.copy(view.up);
controls.target.copy(newTarget);
controls.update();
}
};
animateTransition();
} else {
// Sin animación (cambio instantáneo)
camera.position.copy(newPosition);
camera.quaternion.copy(view.quaternion);
camera.up.copy(view.up);
controls.target.copy(newTarget);
controls.update();
}
SESSION.currentView = direction;
updateCompassUI();
updateWallOpacities(); // Actualizar opacidades de paredes según nueva vista
}
// Establecer vista inicial
@@ -587,18 +728,20 @@ async function renderRoom(room) {
const wallGeometry = new THREE.PlaneGeometry(worldWidth, wallHeight);
const wallConfigs = [
{ side: 'N', offset: { x: 0, z: -halfSizeZ }, rotation: 0, opacity: 1.0 },
{ side: 'S', offset: { x: 0, z: halfSizeZ }, rotation: 0, opacity: 0.5 },
{ side: 'E', offset: { x: halfSizeX, z: 0 }, rotation: Math.PI / 2, opacity: 0.5 },
{ side: 'W', offset: { x: -halfSizeX, z: 0 }, rotation: Math.PI / 2, opacity: 1.0 }
{ side: 'N', offset: { x: 0, z: -halfSizeZ }, rotation: 0 },
{ side: 'S', offset: { x: 0, z: halfSizeZ }, rotation: 0 },
{ side: 'E', offset: { x: halfSizeX, z: 0 }, rotation: Math.PI / 2 },
{ side: 'W', offset: { x: -halfSizeX, z: 0 }, rotation: Math.PI / 2 }
];
for (const config of wallConfigs) {
if (room.walls.includes(config.side)) {
const opacity = getWallOpacity(config.side, SESSION.currentView);
const wallMaterial = new THREE.MeshStandardMaterial({
map: wallTex.clone(),
transparent: config.opacity < 1.0,
opacity: config.opacity,
transparent: opacity < 1.0,
opacity: opacity,
side: THREE.DoubleSide
});
@@ -611,6 +754,7 @@ async function renderRoom(room) {
wall.rotation.y = config.rotation;
wall.castShadow = true;
wall.receiveShadow = true;
wall.userData.wallSide = config.side; // Metadata para identificar el lado
scene.add(wall);
roomMeshes.walls.push(wall);
}
@@ -622,6 +766,13 @@ async function renderRoom(room) {
const doorHeight = 2.0;
for (const door of room.doors) {
// Verificar que la sala destino existe
const targetRoom = ROOMS.rooms.find(r => r.id === door.leadsTo);
if (!targetRoom) {
console.warn(`Puerta en sala ${room.id} apunta a sala inexistente ${door.leadsTo}`);
continue; // Saltar esta puerta
}
const doorGeometry = new THREE.PlaneGeometry(doorWidth, doorHeight);
const doorMaterial = new THREE.MeshStandardMaterial({
map: doorTex.clone(),