|
|
|
|
@@ -12,6 +12,9 @@ const CONFIG = {
|
|
|
|
|
const ASSETS = {
|
|
|
|
|
tiles: {
|
|
|
|
|
'tile_base': { src: '/assets/images/tiles/tile4x4.png', width: 4, height: 4 },
|
|
|
|
|
'tile_8x4': { src: '/assets/images/tiles/tile4x4.png', width: 8, height: 4 },
|
|
|
|
|
'tile_4x8': { src: '/assets/images/tiles/tile4x4.png', width: 4, height: 8 },
|
|
|
|
|
'tile_8x8': { src: '/assets/images/tiles/tile4x4.png', width: 8, height: 8 },
|
|
|
|
|
'tile_cyan': { src: '/assets/images/tiles/tile4x4_blue.png', width: 4, height: 4 },
|
|
|
|
|
'tile_orange': { src: '/assets/images/tiles/tile4x4_orange.png', width: 4, height: 4 },
|
|
|
|
|
'wall_1': { src: '/assets/images/tiles/pared1.png' },
|
|
|
|
|
@@ -27,37 +30,98 @@ const ASSETS = {
|
|
|
|
|
// --- GENERADOR PROCEDURAL DE MAZMORRAS ---
|
|
|
|
|
function generateDungeon() {
|
|
|
|
|
const rooms = [];
|
|
|
|
|
const maxRooms = 10;
|
|
|
|
|
const tileTypes = ['tile_base', 'tile_base', 'tile_base']; // Solo tile_base (4x4)
|
|
|
|
|
const maxRooms = 15;
|
|
|
|
|
|
|
|
|
|
// Configuración de reglas de generación (Pesos y Límites)
|
|
|
|
|
const GENERATION_RULES = {
|
|
|
|
|
'tile_base': { weight: 60, max: Infinity }, // 4x4 (Muy común)
|
|
|
|
|
'tile_8x4': { weight: 15, max: Infinity }, // Pasillo H (Medio)
|
|
|
|
|
'tile_4x8': { weight: 15, max: Infinity }, // Pasillo V (Medio)
|
|
|
|
|
'tile_8x8': { weight: 10, max: 2 } // Sala Grande (Rara, max 2)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let entityIdCounter = 100;
|
|
|
|
|
|
|
|
|
|
// Sala inicial (siempre en 0,0 con el héroe)
|
|
|
|
|
// Helper para verificar si un área está libre
|
|
|
|
|
// Ocupación se guarda como strings "x,y" para cada bloque de 4x4
|
|
|
|
|
const occupied = new Set();
|
|
|
|
|
|
|
|
|
|
function markOccupied(x, y, width, height) {
|
|
|
|
|
for (let i = 0; i < width; i += 4) {
|
|
|
|
|
for (let j = 0; j < height; j += 4) {
|
|
|
|
|
occupied.add(`${x + i},${y + j}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isAreaFree(x, y, width, height) {
|
|
|
|
|
for (let i = 0; i < width; i += 4) {
|
|
|
|
|
for (let j = 0; j < height; j += 4) {
|
|
|
|
|
if (occupied.has(`${x + i},${y + j}`)) return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper para elegir tipo de sala según pesos
|
|
|
|
|
function pickRandomRoomType() {
|
|
|
|
|
// 1. Contar cuántas de cada tipo tenemos ya
|
|
|
|
|
const currentCounts = {};
|
|
|
|
|
Object.keys(GENERATION_RULES).forEach(k => currentCounts[k] = 0);
|
|
|
|
|
rooms.forEach(r => {
|
|
|
|
|
if (currentCounts[r.tile.type] !== undefined) {
|
|
|
|
|
currentCounts[r.tile.type]++;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 2. Filtrar candidatos válidos (que no superen su max)
|
|
|
|
|
const candidates = Object.keys(GENERATION_RULES).filter(type => {
|
|
|
|
|
return currentCounts[type] < GENERATION_RULES[type].max;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 3. Calcular peso total de los candidatos
|
|
|
|
|
const totalWeight = candidates.reduce((sum, type) => sum + GENERATION_RULES[type].weight, 0);
|
|
|
|
|
|
|
|
|
|
// 4. Elegir aleatoriamente
|
|
|
|
|
let random = Math.random() * totalWeight;
|
|
|
|
|
|
|
|
|
|
for (const type of candidates) {
|
|
|
|
|
random -= GENERATION_RULES[type].weight;
|
|
|
|
|
if (random <= 0) {
|
|
|
|
|
return type;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 'tile_base'; // Fallback por seguridad
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sala inicial (siempre 4x4 en 0,0 con el héroe)
|
|
|
|
|
const startTileKey = 'tile_base';
|
|
|
|
|
rooms.push({
|
|
|
|
|
id: 1,
|
|
|
|
|
tile: { type: 'tile_base', x: 0, y: 0 },
|
|
|
|
|
tile: { type: startTileKey, x: 0, y: 0 },
|
|
|
|
|
walls: ['N', 'S', 'E', 'W'],
|
|
|
|
|
doors: [],
|
|
|
|
|
entities: [{ id: entityIdCounter++, type: 'hero_1', x: 1, y: 1 }]
|
|
|
|
|
});
|
|
|
|
|
// Marcar ocupado el área de la sala inicial
|
|
|
|
|
markOccupied(0, 0, ASSETS.tiles[startTileKey].width, ASSETS.tiles[startTileKey].height);
|
|
|
|
|
|
|
|
|
|
// Direcciones posibles: N, S, E, W
|
|
|
|
|
// Nota: dx/dy se calcularán dinámicamente
|
|
|
|
|
const directions = [
|
|
|
|
|
{ side: 'N', dx: 0, dy: -4, opposite: 'S' },
|
|
|
|
|
{ side: 'S', dx: 0, dy: 4, opposite: 'N' },
|
|
|
|
|
{ side: 'E', dx: 4, dy: 0, opposite: 'W' },
|
|
|
|
|
{ side: 'W', dx: -4, dy: 0, opposite: 'E' }
|
|
|
|
|
{ side: 'N', opposite: 'S' },
|
|
|
|
|
{ side: 'S', opposite: 'N' },
|
|
|
|
|
{ side: 'E', opposite: 'W' },
|
|
|
|
|
{ side: 'W', opposite: 'E' }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Posiciones ocupadas (para evitar solapamientos)
|
|
|
|
|
const occupied = new Set(['0,0']);
|
|
|
|
|
|
|
|
|
|
// Cola de salas pendientes de expandir
|
|
|
|
|
const queue = [{ roomId: 1, x: 0, y: 0 }];
|
|
|
|
|
// Cola de salas para expandir
|
|
|
|
|
const queue = [rooms[0]];
|
|
|
|
|
|
|
|
|
|
while (rooms.length < maxRooms && queue.length > 0) {
|
|
|
|
|
const current = queue.shift();
|
|
|
|
|
const currentRoom = rooms.find(r => r.id === current.roomId);
|
|
|
|
|
const currentRoom = queue.shift();
|
|
|
|
|
const currentTileDef = ASSETS.tiles[currentRoom.tile.type];
|
|
|
|
|
|
|
|
|
|
// Intentar añadir salas en direcciones aleatorias
|
|
|
|
|
const shuffledDirections = [...directions].sort(() => Math.random() - 0.5);
|
|
|
|
|
@@ -65,32 +129,53 @@ function generateDungeon() {
|
|
|
|
|
for (const dir of shuffledDirections) {
|
|
|
|
|
if (rooms.length >= maxRooms) break;
|
|
|
|
|
|
|
|
|
|
const newX = current.x + dir.dx;
|
|
|
|
|
const newY = current.y + dir.dy;
|
|
|
|
|
const posKey = `${newX},${newY}`;
|
|
|
|
|
// Selección ponderada del tipo de sala
|
|
|
|
|
const nextTileType = pickRandomRoomType();
|
|
|
|
|
const nextTileDef = ASSETS.tiles[nextTileType];
|
|
|
|
|
|
|
|
|
|
// Verificar que no esté ocupada
|
|
|
|
|
if (occupied.has(posKey)) continue;
|
|
|
|
|
// Calcular posición de la nueva sala según la dirección
|
|
|
|
|
let newX, newY;
|
|
|
|
|
|
|
|
|
|
// 60% de probabilidad de crear sala en esta dirección (aumentado para más conectividad)
|
|
|
|
|
if (Math.random() < 0.4) continue;
|
|
|
|
|
// Estrategia de alineación: Alineamos siempre a "top-left" relativo a la dirección de crecimiento.
|
|
|
|
|
// Esto asegura que al menos un segmento de 4x4 coincida para poner la puerta.
|
|
|
|
|
if (dir.side === 'N') {
|
|
|
|
|
newX = currentRoom.tile.x; // Alineado a la izquierda
|
|
|
|
|
newY = currentRoom.tile.y - nextTileDef.height;
|
|
|
|
|
} else if (dir.side === 'S') {
|
|
|
|
|
newX = currentRoom.tile.x; // Alineado a la izquierda
|
|
|
|
|
newY = currentRoom.tile.y + currentTileDef.height;
|
|
|
|
|
} else if (dir.side === 'E') {
|
|
|
|
|
newX = currentRoom.tile.x + currentTileDef.width;
|
|
|
|
|
newY = currentRoom.tile.y; // Alineado arriba
|
|
|
|
|
} else if (dir.side === 'W') {
|
|
|
|
|
newX = currentRoom.tile.x - nextTileDef.width;
|
|
|
|
|
newY = currentRoom.tile.y; // Alineado arriba
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verificar si el área está libre
|
|
|
|
|
if (!isAreaFree(newX, newY, nextTileDef.width, nextTileDef.height)) continue;
|
|
|
|
|
|
|
|
|
|
// 40% de probabilidad de no crear sala en esta dirección (si hay espacio)
|
|
|
|
|
// reducimos la probabilidad de fallo para fomentar estructura más densa con salas grandes
|
|
|
|
|
if (Math.random() < 0.3) continue;
|
|
|
|
|
|
|
|
|
|
// Crear nueva sala
|
|
|
|
|
const newRoomId = rooms.length + 1;
|
|
|
|
|
const tileType = tileTypes[Math.floor(Math.random() * tileTypes.length)];
|
|
|
|
|
|
|
|
|
|
// Generar 0, 1 o 2 esqueletos aleatorios
|
|
|
|
|
const numSkeletons = Math.floor(Math.random() * 3); // 0, 1, o 2
|
|
|
|
|
// Generar entidades (esqueletos)
|
|
|
|
|
// En salas grandes ponemos más bichos potencialmente
|
|
|
|
|
const areaFactor = (nextTileDef.width * nextTileDef.height) / 16;
|
|
|
|
|
const maxSkeletons = Math.floor(2 * areaFactor);
|
|
|
|
|
const numSkeletons = Math.floor(Math.random() * (maxSkeletons + 1));
|
|
|
|
|
const newEntities = [];
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < numSkeletons; i++) {
|
|
|
|
|
// Posición aleatoria dentro de la tile 4x4
|
|
|
|
|
const randomX = newX + Math.floor(Math.random() * 4);
|
|
|
|
|
const randomY = newY + Math.floor(Math.random() * 4);
|
|
|
|
|
const randomX = newX + Math.floor(Math.random() * nextTileDef.width);
|
|
|
|
|
const randomY = newY + Math.floor(Math.random() * nextTileDef.height);
|
|
|
|
|
|
|
|
|
|
newEntities.push({
|
|
|
|
|
id: entityIdCounter++,
|
|
|
|
|
type: 'hero_2', // esqueleto
|
|
|
|
|
type: 'hero_2',
|
|
|
|
|
x: randomX,
|
|
|
|
|
y: randomY
|
|
|
|
|
});
|
|
|
|
|
@@ -98,44 +183,51 @@ function generateDungeon() {
|
|
|
|
|
|
|
|
|
|
const newRoom = {
|
|
|
|
|
id: newRoomId,
|
|
|
|
|
tile: { type: tileType, x: newX, y: newY },
|
|
|
|
|
tile: { type: nextTileType, x: newX, y: newY },
|
|
|
|
|
walls: ['N', 'S', 'E', 'W'],
|
|
|
|
|
doors: [],
|
|
|
|
|
entities: newEntities
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Añadir la sala primero
|
|
|
|
|
// Añadir y marcar
|
|
|
|
|
rooms.push(newRoom);
|
|
|
|
|
occupied.add(posKey);
|
|
|
|
|
queue.push({ roomId: newRoomId, x: newX, y: newY });
|
|
|
|
|
markOccupied(newX, newY, nextTileDef.width, nextTileDef.height);
|
|
|
|
|
queue.push(newRoom);
|
|
|
|
|
|
|
|
|
|
// CREAR PUERTAS
|
|
|
|
|
// Siempre ponemos la puerta en los primeros 4 tiles de la conexión, que sabemos que existen por la alineación.
|
|
|
|
|
// gridPos entre 1 y 2 (dejando margenes de 1 celda en bordes de 4)
|
|
|
|
|
const doorGridPos = Math.floor(Math.random() * 2) + 1;
|
|
|
|
|
|
|
|
|
|
// Puerta en la sala actual (origen)
|
|
|
|
|
// Ojo: gridX/Y es relativo al origen de la sala.
|
|
|
|
|
// Como alineamos coordenadas:
|
|
|
|
|
// N/S: Alineados en X -> puerta en X relativo es igual para ambos.
|
|
|
|
|
// E/W: Alineados en Y -> puerta en Y relativo es igual para ambos.
|
|
|
|
|
|
|
|
|
|
// AHORA crear puertas (solo si la sala fue creada)
|
|
|
|
|
const doorGridPos = Math.floor(Math.random() * 2) + 1; // Posición 1 o 2
|
|
|
|
|
const doorConfig = dir.side === 'N' || dir.side === 'S'
|
|
|
|
|
? { side: dir.side, gridX: doorGridPos, leadsTo: newRoomId }
|
|
|
|
|
: { side: dir.side, gridY: doorGridPos, leadsTo: newRoomId };
|
|
|
|
|
|
|
|
|
|
currentRoom.doors.push(doorConfig);
|
|
|
|
|
|
|
|
|
|
// Crear puerta en la nueva sala hacia la actual
|
|
|
|
|
// 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: current.roomId }
|
|
|
|
|
: { side: dir.opposite, gridY: doorGridPos, leadsTo: current.roomId };
|
|
|
|
|
? { side: dir.opposite, gridX: doorGridPos, leadsTo: currentRoom.id }
|
|
|
|
|
: { side: dir.opposite, gridY: doorGridPos, leadsTo: currentRoom.id };
|
|
|
|
|
|
|
|
|
|
newRoom.doors.push(oppositeDoorConfig);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Limpiar puertas que apuntan a salas inexistentes
|
|
|
|
|
// Limpiar puertas inválidas (paranoia check)
|
|
|
|
|
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;
|
|
|
|
|
});
|
|
|
|
|
room.doors = room.doors.filter(door => existingRoomIds.has(door.leadsTo));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
@@ -270,18 +362,16 @@ function setCameraView(direction, animate = true) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calcular offset de la vista (diferencia entre posición y target)
|
|
|
|
|
// Calcular offset de la vista (diferencia entre posición y target definidos en CAMERA_VIEWS)
|
|
|
|
|
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();
|
|
|
|
|
const targetPosition = playerPosition.clone().add(viewOffset);
|
|
|
|
|
const targetLookAt = 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 startPosition = camera.position.clone();
|
|
|
|
|
const startLookAt = controls.target.clone();
|
|
|
|
|
|
|
|
|
|
const duration = 600; // ms
|
|
|
|
|
const startTime = Date.now();
|
|
|
|
|
@@ -290,48 +380,47 @@ function setCameraView(direction, animate = true) {
|
|
|
|
|
const elapsed = Date.now() - startTime;
|
|
|
|
|
const progress = Math.min(elapsed / duration, 1);
|
|
|
|
|
|
|
|
|
|
// Easing suave (ease-in-out)
|
|
|
|
|
// Easing suave
|
|
|
|
|
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);
|
|
|
|
|
// Interpolación LINEAL de posición y target
|
|
|
|
|
const currentPos = new THREE.Vector3().lerpVectors(startPosition, targetPosition, eased);
|
|
|
|
|
const currentLookAt = new THREE.Vector3().lerpVectors(startLookAt, targetLookAt, eased);
|
|
|
|
|
|
|
|
|
|
// Interpolar quaternion (rotación suave)
|
|
|
|
|
camera.quaternion.slerpQuaternions(startQuat, view.quaternion, eased);
|
|
|
|
|
camera.position.copy(currentPos);
|
|
|
|
|
camera.up.set(0, 1, 0); // FORZAR UP VECTOR SIEMPRE
|
|
|
|
|
camera.lookAt(currentLookAt);
|
|
|
|
|
|
|
|
|
|
// Interpolar target
|
|
|
|
|
controls.target.lerpVectors(startTarget, newTarget, eased);
|
|
|
|
|
|
|
|
|
|
camera.up.copy(view.up);
|
|
|
|
|
controls.target.copy(currentLookAt);
|
|
|
|
|
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);
|
|
|
|
|
// Asegurar estado final perfecto
|
|
|
|
|
camera.position.copy(targetPosition);
|
|
|
|
|
camera.up.set(0, 1, 0);
|
|
|
|
|
camera.lookAt(targetLookAt);
|
|
|
|
|
controls.target.copy(targetLookAt);
|
|
|
|
|
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);
|
|
|
|
|
// Cambio inmediato
|
|
|
|
|
camera.position.copy(targetPosition);
|
|
|
|
|
camera.up.set(0, 1, 0); // FORZAR UP VECTOR
|
|
|
|
|
camera.lookAt(targetLookAt);
|
|
|
|
|
controls.target.copy(targetLookAt);
|
|
|
|
|
controls.update();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SESSION.currentView = direction;
|
|
|
|
|
updateCompassUI();
|
|
|
|
|
updateWallOpacities(); // Actualizar opacidades de paredes según nueva vista
|
|
|
|
|
updateWallOpacities();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Establecer vista inicial
|
|
|
|
|
@@ -564,9 +653,15 @@ async function animateMovement() {
|
|
|
|
|
unit.x = step.x;
|
|
|
|
|
unit.y = step.y;
|
|
|
|
|
|
|
|
|
|
// Verificar si hemos llegado a una puerta
|
|
|
|
|
// 1. Verificar si hemos pisado una puerta (Para renderizar lo siguiente antes de entrar)
|
|
|
|
|
checkDoorTransition(unit, unitRoom);
|
|
|
|
|
|
|
|
|
|
// 2. AUTO-CORRECCIÓN: Verificar en qué sala estamos FÍSICAMENTE
|
|
|
|
|
const actualRoom = detectRoomChange(unit, unitRoom);
|
|
|
|
|
if (actualRoom) {
|
|
|
|
|
unitRoom = actualRoom;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SESSION.path.shift();
|
|
|
|
|
updatePathVisuals();
|
|
|
|
|
}
|
|
|
|
|
@@ -580,13 +675,47 @@ async function animateMovement() {
|
|
|
|
|
controls.target.copy(newTarget);
|
|
|
|
|
camera.position.copy(newTarget).add(currentOffset);
|
|
|
|
|
|
|
|
|
|
// Actualizar el target de la vista actual para futuras referencias
|
|
|
|
|
CAMERA_VIEWS[SESSION.currentView].target = { x: newTarget.x, y: newTarget.y, z: newTarget.z };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
SESSION.selectedUnitId = null;
|
|
|
|
|
updateSelectionVisuals();
|
|
|
|
|
SESSION.isAnimating = false;
|
|
|
|
|
drawMinimap(); // Actualizar posición final del jugador
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verifica si la unidad ha entrado físicamente en una sala diferente a la registrada
|
|
|
|
|
function detectRoomChange(unit, currentLogicalRoom) {
|
|
|
|
|
for (const room of ROOMS.rooms) {
|
|
|
|
|
if (room.id === currentLogicalRoom.id) continue;
|
|
|
|
|
|
|
|
|
|
if (isPositionInRoom(unit.x, unit.y, room)) {
|
|
|
|
|
console.log(`CORRECCIÓN: Entidad detectada en sala ${room.id} (registrada en ${currentLogicalRoom.id}). Transfiriendo...`);
|
|
|
|
|
|
|
|
|
|
// Transferir entidad lógica
|
|
|
|
|
const oldIdx = currentLogicalRoom.entities.indexOf(unit);
|
|
|
|
|
if (oldIdx > -1) currentLogicalRoom.entities.splice(oldIdx, 1);
|
|
|
|
|
room.entities.push(unit);
|
|
|
|
|
|
|
|
|
|
// Transferir mesh (para renderizado/borrado correcto)
|
|
|
|
|
if (SESSION.roomMeshes[currentLogicalRoom.id]) {
|
|
|
|
|
const meshIdx = SESSION.roomMeshes[currentLogicalRoom.id].entities.indexOf(unit.mesh);
|
|
|
|
|
if (meshIdx > -1) SESSION.roomMeshes[currentLogicalRoom.id].entities.splice(meshIdx, 1);
|
|
|
|
|
}
|
|
|
|
|
if (SESSION.roomMeshes[room.id]) {
|
|
|
|
|
SESSION.roomMeshes[room.id].entities.push(unit.mesh);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Asegurar que la sala está visitada y renderizada (si llegamos aquí por "magia")
|
|
|
|
|
if (!ROOMS.visitedRooms.has(room.id)) {
|
|
|
|
|
ROOMS.visitedRooms.add(room.id);
|
|
|
|
|
renderRoom(room);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ROOMS.currentRoom = room.id;
|
|
|
|
|
drawMinimap();
|
|
|
|
|
|
|
|
|
|
return room; // Devolver la nueva sala actual
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- VERIFICAR TRANSICIÓN DE PUERTA ---
|
|
|
|
|
@@ -597,40 +726,16 @@ function checkDoorTransition(unit, currentRoom) {
|
|
|
|
|
if (unit.x === doorGridPos.x && unit.y === doorGridPos.y) {
|
|
|
|
|
const targetRoomId = door.leadsTo;
|
|
|
|
|
|
|
|
|
|
// Solo nos encargamos de precargar/revelar la sala aquí
|
|
|
|
|
if (!ROOMS.visitedRooms.has(targetRoomId)) {
|
|
|
|
|
console.log("Puerta pisada -> Revelando sala", targetRoomId);
|
|
|
|
|
ROOMS.visitedRooms.add(targetRoomId);
|
|
|
|
|
const targetRoom = ROOMS.rooms.find(r => r.id === targetRoomId);
|
|
|
|
|
if (targetRoom) {
|
|
|
|
|
renderRoom(targetRoom);
|
|
|
|
|
drawMinimap();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mover entidad a la nueva sala
|
|
|
|
|
const targetRoom = ROOMS.rooms.find(r => r.id === targetRoomId);
|
|
|
|
|
if (targetRoom) {
|
|
|
|
|
const entityIndex = currentRoom.entities.indexOf(unit);
|
|
|
|
|
if (entityIndex > -1) {
|
|
|
|
|
currentRoom.entities.splice(entityIndex, 1);
|
|
|
|
|
|
|
|
|
|
// Actualizar tracking de meshes
|
|
|
|
|
if (SESSION.roomMeshes[currentRoom.id]) {
|
|
|
|
|
const meshIndex = SESSION.roomMeshes[currentRoom.id].entities.indexOf(unit.mesh);
|
|
|
|
|
if (meshIndex > -1) {
|
|
|
|
|
SESSION.roomMeshes[currentRoom.id].entities.splice(meshIndex, 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
targetRoom.entities.push(unit);
|
|
|
|
|
|
|
|
|
|
// Añadir mesh al tracking de la nueva sala
|
|
|
|
|
if (SESSION.roomMeshes[targetRoomId]) {
|
|
|
|
|
SESSION.roomMeshes[targetRoomId].entities.push(unit.mesh);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ROOMS.currentRoom = targetRoomId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -673,6 +778,7 @@ function loadTexture(path) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function renderRoom(room) {
|
|
|
|
|
console.log(">>> renderRoom ejecutado para sala:", room.id);
|
|
|
|
|
if (SESSION.roomMeshes[room.id]) {
|
|
|
|
|
return; // Ya renderizada
|
|
|
|
|
}
|
|
|
|
|
@@ -684,12 +790,21 @@ async function renderRoom(room) {
|
|
|
|
|
entities: []
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Renderizar tile
|
|
|
|
|
// Renderizar tile
|
|
|
|
|
const tileDef = ASSETS.tiles[room.tile.type];
|
|
|
|
|
const tileTex = await loadTexture(tileDef.src);
|
|
|
|
|
const baseTex = await loadTexture(tileDef.src);
|
|
|
|
|
const tileTex = baseTex.clone(); // CLONAR para no afectar a otras salas
|
|
|
|
|
tileTex.needsUpdate = true; // Asegurar que Three.js sepa que es nueva
|
|
|
|
|
|
|
|
|
|
tileTex.wrapS = THREE.RepeatWrapping;
|
|
|
|
|
tileTex.wrapT = THREE.RepeatWrapping;
|
|
|
|
|
tileTex.repeat.set(tileDef.width / 2, tileDef.height / 2);
|
|
|
|
|
|
|
|
|
|
// Lógica de repetición: La textura base es de 4x4 celdas.
|
|
|
|
|
// Si la sala es 8x4, repetimos 2 en X, 1 en Y.
|
|
|
|
|
const repeatX = tileDef.width / 4;
|
|
|
|
|
const repeatY = tileDef.height / 4;
|
|
|
|
|
tileTex.repeat.set(repeatX, repeatY);
|
|
|
|
|
|
|
|
|
|
const worldWidth = tileDef.width * CONFIG.CELL_SIZE;
|
|
|
|
|
const worldHeight = tileDef.height * CONFIG.CELL_SIZE;
|
|
|
|
|
@@ -717,6 +832,7 @@ async function renderRoom(room) {
|
|
|
|
|
const wallTex = await loadTexture(ASSETS.tiles['wall_1'].src);
|
|
|
|
|
wallTex.wrapS = THREE.RepeatWrapping;
|
|
|
|
|
wallTex.wrapT = THREE.RepeatWrapping;
|
|
|
|
|
// Las paredes siempre repiten en horizontal, la V es fija
|
|
|
|
|
wallTex.repeat.set(2, 2);
|
|
|
|
|
|
|
|
|
|
const wallHeight = 2.5;
|
|
|
|
|
@@ -725,26 +841,32 @@ async function renderRoom(room) {
|
|
|
|
|
const centerX = tileMesh.position.x;
|
|
|
|
|
const centerZ = tileMesh.position.z;
|
|
|
|
|
|
|
|
|
|
const wallGeometry = new THREE.PlaneGeometry(worldWidth, wallHeight);
|
|
|
|
|
|
|
|
|
|
const wallConfigs = [
|
|
|
|
|
{ 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 }
|
|
|
|
|
{ side: 'N', width: worldWidth, offset: { x: 0, z: -halfSizeZ }, rotation: 0 },
|
|
|
|
|
{ side: 'S', width: worldWidth, offset: { x: 0, z: halfSizeZ }, rotation: 0 },
|
|
|
|
|
{ side: 'E', width: worldHeight, offset: { x: halfSizeX, z: 0 }, rotation: Math.PI / 2 },
|
|
|
|
|
{ side: 'W', width: worldHeight, 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);
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
const wallMaterial = new THREE.MeshStandardMaterial({
|
|
|
|
|
map: wallTex.clone(),
|
|
|
|
|
map: materialTex,
|
|
|
|
|
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(wallGeometry, wallMaterial);
|
|
|
|
|
wall.position.set(
|
|
|
|
|
centerX + config.offset.x,
|
|
|
|
|
@@ -868,6 +990,107 @@ function updateCompassUI() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- MINIMAP UI ---
|
|
|
|
|
function drawMinimap() {
|
|
|
|
|
const canvas = document.getElementById('minimap');
|
|
|
|
|
if (!canvas) return;
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
const width = canvas.width;
|
|
|
|
|
const height = canvas.height;
|
|
|
|
|
|
|
|
|
|
// Limpiar canvas
|
|
|
|
|
ctx.fillStyle = '#111';
|
|
|
|
|
ctx.fillRect(0, 0, width, height);
|
|
|
|
|
|
|
|
|
|
if (ROOMS.rooms.length === 0) return;
|
|
|
|
|
|
|
|
|
|
// 1. Calcular límites del MAPA COMPLETO (Debug Mode: Ver todo)
|
|
|
|
|
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
|
|
|
|
|
|
|
|
ROOMS.rooms.forEach(room => {
|
|
|
|
|
const tileDef = ASSETS.tiles[room.tile.type];
|
|
|
|
|
minX = Math.min(minX, room.tile.x);
|
|
|
|
|
maxX = Math.max(maxX, room.tile.x + tileDef.width);
|
|
|
|
|
minY = Math.min(minY, room.tile.y);
|
|
|
|
|
maxY = Math.max(maxY, room.tile.y + tileDef.height);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Añadir margen generoso para ver bien
|
|
|
|
|
const margin = 8;
|
|
|
|
|
minX -= margin;
|
|
|
|
|
maxX += margin;
|
|
|
|
|
minY -= margin;
|
|
|
|
|
maxY += margin;
|
|
|
|
|
|
|
|
|
|
const mapWidth = maxX - minX;
|
|
|
|
|
const mapHeight = maxY - minY;
|
|
|
|
|
|
|
|
|
|
// Calcular escala para encajar TODO el mapa en el canvas
|
|
|
|
|
const scaleX = width / mapWidth;
|
|
|
|
|
const scaleY = height / mapHeight;
|
|
|
|
|
const scale = Math.min(scaleX, scaleY);
|
|
|
|
|
|
|
|
|
|
// Función para transformar coords de cuadrícula a canvas
|
|
|
|
|
const toCanvas = (x, y) => {
|
|
|
|
|
return {
|
|
|
|
|
x: (x - minX) * scale + (width - mapWidth * scale) / 2,
|
|
|
|
|
y: (y - minY) * scale + (height - mapHeight * scale) / 2
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 2. Dibujar TODAS las Salas
|
|
|
|
|
ROOMS.rooms.forEach(room => {
|
|
|
|
|
const tileDef = ASSETS.tiles[room.tile.type];
|
|
|
|
|
|
|
|
|
|
const pos = toCanvas(room.tile.x, room.tile.y);
|
|
|
|
|
const w = tileDef.width * scale;
|
|
|
|
|
const h = tileDef.height * scale;
|
|
|
|
|
|
|
|
|
|
// Color base de sala
|
|
|
|
|
const isVisited = ROOMS.visitedRooms.has(room.id);
|
|
|
|
|
|
|
|
|
|
if (room.id === ROOMS.currentRoom) {
|
|
|
|
|
ctx.fillStyle = '#44aadd'; // Actual: Azul
|
|
|
|
|
} else if (isVisited) {
|
|
|
|
|
ctx.fillStyle = '#777'; // Visitada: Gris Claro
|
|
|
|
|
} else {
|
|
|
|
|
ctx.fillStyle = '#333'; // No Visitada: Gris Oscuro
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.fillRect(pos.x, pos.y, w, h);
|
|
|
|
|
|
|
|
|
|
// Borde
|
|
|
|
|
ctx.strokeStyle = '#555';
|
|
|
|
|
ctx.lineWidth = 1;
|
|
|
|
|
ctx.strokeRect(pos.x, pos.y, w, h);
|
|
|
|
|
|
|
|
|
|
// Puertas
|
|
|
|
|
ctx.fillStyle = isVisited ? '#fff' : '#666'; // Puertas tenues si no visitado
|
|
|
|
|
room.doors.forEach(door => {
|
|
|
|
|
const doorGridPos = getDoorGridPosition(room, door);
|
|
|
|
|
const dPos = toCanvas(doorGridPos.x + 0.5, doorGridPos.y + 0.5);
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.arc(dPos.x, dPos.y, scale * 0.4, 0, Math.PI * 2);
|
|
|
|
|
ctx.fill();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 3. Dibujar Jugador
|
|
|
|
|
const currentRoomObj = ROOMS.rooms.find(r => r.id === ROOMS.currentRoom);
|
|
|
|
|
if (currentRoomObj) {
|
|
|
|
|
const player = currentRoomObj.entities.find(e => e.type === 'hero_1');
|
|
|
|
|
if (player) {
|
|
|
|
|
const pPos = toCanvas(player.x + 0.5, player.y + 0.5);
|
|
|
|
|
|
|
|
|
|
ctx.fillStyle = '#ffff00';
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.arc(pPos.x, pPos.y, scale * 0.8, 0, Math.PI * 2);
|
|
|
|
|
ctx.fill();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Event listeners para los botones del compás
|
|
|
|
|
document.querySelectorAll('.compass-btn').forEach(btn => {
|
|
|
|
|
btn.addEventListener('click', () => {
|
|
|
|
|
@@ -876,6 +1099,8 @@ document.querySelectorAll('.compass-btn').forEach(btn => {
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Inicializar minimapa
|
|
|
|
|
drawMinimap();
|
|
|
|
|
|
|
|
|
|
// --- INTERACCIÓN ---
|
|
|
|
|
const raycaster = new THREE.Raycaster();
|
|
|
|
|
|