diff --git a/src/dungeon/DungeonDecks.js b/src/dungeon/DungeonDecks.js new file mode 100644 index 0000000..dd44138 --- /dev/null +++ b/src/dungeon/DungeonDecks.js @@ -0,0 +1,48 @@ +const ROOM_CARDS = [ + { type: 'tile_4x8', width: 4, height: 8, exits: ['N', 'S'], name: "Pasillo Largo" }, + { type: 'tile_base', width: 4, height: 4, exits: ['N', 'E', 'W'], name: "Sala Pequeña" }, + { type: 'tile_base', width: 4, height: 4, exits: ['N', 'S', 'E', 'W'], name: "Intersección" }, + { type: 'tile_8x4', width: 8, height: 4, exits: ['N', 'S'], name: "Sala Ancha" } +]; + +export class DungeonDeck { + constructor() { + this.deck = []; + this.discardPile = []; + this.shuffleDeck(); + } + + shuffleDeck() { + // Create a new deck with multiple copies of cards + this.deck = [ + ...ROOM_CARDS, ...ROOM_CARDS, ...ROOM_CARDS, // 3 copies of each + { type: 'tile_8x8', width: 8, height: 8, exits: ['N'], name: "Sala del Tesoro" } // 1 Objective Room + ]; + + // Fisher-Yates shuffle + for (let i = this.deck.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [this.deck[i], this.deck[j]] = [this.deck[j], this.deck[i]]; + } + console.log("Dungeon Deck Shuffled:", this.deck.length, "cards"); + } + + drawCard() { + if (this.deck.length === 0) { + console.warn("Deck empty! Reshuffling discards..."); + if (this.discardPile.length === 0) { + console.error("No cards left!"); + return null; + } + this.deck = [...this.discardPile]; + this.discardPile = []; + this.shuffleDeck(); + } + + const card = this.deck.pop(); + this.discardPile.push(card); + return card; + } +} + +export const dungeonDeck = new DungeonDeck(); diff --git a/src/dungeon/EventDeck.js b/src/dungeon/EventDeck.js new file mode 100644 index 0000000..65d21a2 --- /dev/null +++ b/src/dungeon/EventDeck.js @@ -0,0 +1,41 @@ +export const EVENTS = [ + { + id: 'ev_nothing', + title: 'Silencio', + description: 'La mazmorra está en calma... sospechosamente tranquila.', + type: 'NADA' + }, + { + id: 'ev_monster_1', + title: '¡Emboscada!', + description: '¡Monstruos surgen de las sombras!', + type: 'MONSTRUO', + count: 2 + }, + { + id: 'ev_trap_1', + title: 'Trampa de Pinchos', + description: 'Un click resuena bajo tus pies. ¡Pinchos surgen del suelo!', + type: 'TRAMPA', + damage: 1 + }, + { + id: 'ev_wind', + title: 'Viento Helado', + description: 'Una corriente de aire apaga vuestras antorchas.', + type: 'PELIGRO' + } +]; + +export class EventDeck { + constructor() { + } + + drawCard() { + // Simple random draw for now + const randIndex = Math.floor(Math.random() * EVENTS.length); + return EVENTS[randIndex]; + } +} + +export const eventDeck = new EventDeck(); diff --git a/src/main.js b/src/main.js index 479ed91..262be3d 100644 --- a/src/main.js +++ b/src/main.js @@ -2,6 +2,9 @@ import './style.css'; import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import { io } from "socket.io-client"; +import { turnManager, PHASES } from './systems/TurnManager.js'; +import { dungeonDeck } from './dungeon/DungeonDecks.js'; +import { eventDeck } from './dungeon/EventDeck.js'; // Import Event Deck // --- NETWORK SETUP --- // Dynamic connection to support playing from mobile on the same network @@ -28,7 +31,7 @@ socket.on("LOBBY_CREATED", ({ code }) => { codeDisplay.style.fontWeight = 'bold'; codeDisplay.style.background = 'rgba(0,0,0,0.5)'; codeDisplay.style.padding = '10px'; - codeDisplay.innerText = `LOBBY: ${code}`; + codeDisplay.innerText = `SALA: ${code}`; document.body.appendChild(codeDisplay); }); @@ -72,212 +75,214 @@ const ASSETS = { // Sistema de salas // --- GENERADOR PROCEDURAL DE MAZMORRAS --- +// --- GENERACIÓN DE MAZMORRA (DINÁMICA) --- function generateDungeon() { - const rooms = []; - 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) + // Start with a single entry room 4x4 + const startRoom = { + id: 1, + tile: { type: 'tile_base', x: 0, y: 0 }, + walls: [], // Walls calculated dynamically later or fixed for start + doors: [ + { side: 'N', gridX: 2, gridY: 0, leadsTo: null, id: 'door_start_N', isOpen: false } + ], + entities: [ + { id: 101, type: 'hero_1', x: 2, y: 2 } + ] }; - let entityIdCounter = 100; - - // 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: 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', opposite: 'S' }, - { side: 'S', opposite: 'N' }, - { side: 'E', opposite: 'W' }, - { side: 'W', opposite: 'E' } - ]; - - // Cola de salas para expandir - const queue = [rooms[0]]; - - while (rooms.length < maxRooms && queue.length > 0) { - 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); - - for (const dir of shuffledDirections) { - if (rooms.length >= maxRooms) break; - - // Selección ponderada del tipo de sala - const nextTileType = pickRandomRoomType(); - const nextTileDef = ASSETS.tiles[nextTileType]; - - // Calcular posición de la nueva sala según la dirección - let newX, newY; - - // 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; - - // 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++) { - 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', - x: randomX, - y: randomY - }); - } - - const newRoom = { - id: newRoomId, - tile: { type: nextTileType, x: newX, y: newY }, - walls: ['N', 'S', 'E', 'W'], - doors: [], - entities: newEntities - }; - - // Añadir y marcar - rooms.push(newRoom); - 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. - - const doorConfig = dir.side === 'N' || dir.side === 'S' - ? { 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); - - const oppositeDoorConfig = dir.opposite === 'N' || dir.opposite === 'S' - ? { 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); - } - } - - // Limpiar puertas inválidas (paranoia check) - const existingRoomIds = new Set(rooms.map(r => r.id)); - rooms.forEach(room => { - room.doors = room.doors.filter(door => existingRoomIds.has(door.leadsTo)); - }); - return { - rooms: rooms, + rooms: [startRoom], visitedRooms: new Set([1]), currentRoom: 1 }; } +// --- EXPLORACIÓN DINÁMICA --- +function exploreRoom(originRoom, door) { + const card = dungeonDeck.drawCard(); + if (!card) return null; + + // Draw an event card + const eventCard = eventDeck.drawCard(); + if (eventCard) { + console.log("Event Card Drawn:", eventCard); + showUIEvent(eventCard); + } + + const nextTileDef = ASSETS.tiles[card.type]; + const newRoomId = ROOMS.rooms.length + 1; + + // 1. Determinar lado de entrada (Opuesto al de salida) + let entrySide; + if (door.side === 'N') entrySide = 'S'; + else if (door.side === 'S') entrySide = 'N'; + else if (door.side === 'E') entrySide = 'W'; + else if (door.side === 'W') entrySide = 'E'; + + // 2. Determinar posición local de la puerta de entrada en la nueva sala + // Centramos la puerta en el muro correspondiente + let entryGridX, entryGridY; + + if (entrySide === 'N') { + entryGridX = Math.floor(nextTileDef.width / 2); + entryGridY = 0; + } else if (entrySide === 'S') { + entryGridX = Math.floor(nextTileDef.width / 2); + entryGridY = nextTileDef.height; + } else if (entrySide === 'E') { + entryGridX = nextTileDef.width; + entryGridY = Math.floor(nextTileDef.height / 2); + } else if (entrySide === 'W') { + entryGridX = 0; + entryGridY = Math.floor(nextTileDef.height / 2); + } + + // 3. Calcular posición absoluta de la nueva sala para que las puertas coincidan + // Fórmula: NewRoomPos = OriginRoomPos + OriginDoorLocalPos - EntryDoorLocalPos + // Esto hace que OriginDoorWorldPos == EntryDoorWorldPos + const newX = originRoom.tile.x + door.gridX - entryGridX; + const newY = originRoom.tile.y + door.gridY - entryGridY; + + // Comprobar colisiones + if (!isAreaFree(newX, newY, nextTileDef.width, nextTileDef.height)) { + console.warn("Cannot place room: Collision detected!"); + return null; + } + + const newRoom = { + id: newRoomId, + tile: { type: card.type, x: newX, y: newY }, + walls: ['N', 'S', 'E', 'W'], + doors: [], + entities: [] + }; + + // Crear la puerta de entrada + const entryDoor = { + side: entrySide, + leadsTo: originRoom.id, + isOpen: true, + id: `door_${newRoomId}_to_${originRoom.id}`, + gridX: entryGridX, + gridY: entryGridY + }; + + newRoom.doors.push(entryDoor); + + // Generar salidas adicionales según la carta + card.exits.forEach(exitDir => { + if (exitDir === entrySide) return; // Ya tenemos esta puerta + + const exitDoor = { + side: exitDir, + leadsTo: null, // Desconocido + isOpen: false, + id: `door_${newRoomId}_${exitDir}` + }; + + // Calcular coordenadas de la puerta en la nueva sala + if (exitDir === 'N') { + exitDoor.gridX = Math.floor(nextTileDef.width / 2); + exitDoor.gridY = 0; + } else if (exitDir === 'S') { + exitDoor.gridX = Math.floor(nextTileDef.width / 2); + exitDoor.gridY = nextTileDef.height; + } else if (exitDir === 'E') { + exitDoor.gridX = nextTileDef.width; + exitDoor.gridY = Math.floor(nextTileDef.height / 2); + } else if (exitDir === 'W') { + exitDoor.gridX = 0; + exitDoor.gridY = Math.floor(nextTileDef.height / 2); + } + + newRoom.doors.push(exitDoor); + }); + + ROOMS.rooms.push(newRoom); + return newRoom; +} + const ROOMS = generateDungeon(); +// --- TURN SYSTEM UI --- +const phaseDisplay = document.createElement('div'); +phaseDisplay.style.position = 'absolute'; +phaseDisplay.style.top = '10px'; +phaseDisplay.style.left = '50%'; +phaseDisplay.style.transform = 'translateX(-50%)'; +phaseDisplay.style.color = '#fff'; +phaseDisplay.style.fontSize = '24px'; +phaseDisplay.style.fontWeight = 'bold'; +phaseDisplay.style.textShadow = '0 0 5px #000'; +phaseDisplay.style.pointerEvents = 'none'; +document.body.appendChild(phaseDisplay); + +function showUIEvent(event) { + const toast = document.createElement('div'); + toast.style.position = 'absolute'; + toast.style.top = '20%'; + toast.style.left = '50%'; + toast.style.transform = 'translateX(-50%)'; + toast.style.background = 'rgba(0, 0, 0, 0.8)'; + toast.style.color = '#ffcc00'; + toast.style.padding = '20px'; + toast.style.border = '2px solid #ffcc00'; + toast.style.borderRadius = '10px'; + toast.style.textAlign = 'center'; + toast.style.zIndex = '1000'; + + toast.innerHTML = ` +

${event.title}

+

${event.description}

+ ${event.type} + `; + + document.body.appendChild(toast); + + setTimeout(() => { + toast.remove(); + }, 4000); +} + +const PHASE_TRANSLATIONS = { + 'POWER': 'PODER', + 'HERO': 'HÉROES', + 'EXPLORATION': 'EXPLORACIÓN', + 'MONSTER': 'MONSTRUOS', + 'END': 'FIN' +}; + +turnManager.addEventListener('phaseChange', (e) => { + const phaseName = PHASE_TRANSLATIONS[e.detail] || e.detail; + phaseDisplay.innerText = `FASE: ${phaseName}`; + + // Example: Update lighting or UI based on phase + if (e.detail === PHASES.EXPLORATION) { + console.log("Entering Exploration Mode - Waiting for door interaction..."); + } +}); + +turnManager.addEventListener('message', (e) => { + // Show smaller toast for messages + const msg = document.createElement('div'); + msg.style.position = 'absolute'; + msg.style.bottom = '100px'; + msg.style.left = '50%'; + msg.style.transform = 'translateX(-50%)'; + msg.style.color = '#fff'; + msg.style.textShadow = '0 0 2px #000'; + msg.innerText = e.detail; + document.body.appendChild(msg); + setTimeout(() => msg.remove(), 2000); +}); + +turnManager.addEventListener('eventTriggered', (e) => { + showUIEvent(e.detail.event); +}); + +// Start the game loop +turnManager.startTurn(); + const SESSION = { selectedUnitId: null, selectedDoorId: null, // Nuevo: ID de la puerta seleccionada @@ -518,6 +523,39 @@ function isPositionInRoom(x, y, room) { return x >= minX && x <= maxX && y >= minY && y <= maxY; } +// Verificar colisión entre un rectángulo propuesto y las salas existentes +// x, y: Coordenadas Grid (Top-Left) +// width, height: Dimensiones Grid +function isAreaFree(x, y, width, height) { + // Definir Rectángulo A (Propuesto) + // Usamos buffer de 0.1 para evitar contactos exactos que no son solapamientos + // Pero en Grid discreto, strict inequality es mejor. + const aMinX = x; + const aMaxX = x + width; + const aMinY = y; + const aMaxY = y + height; + + for (const room of ROOMS.rooms) { + const tileDef = ASSETS.tiles[room.tile.type]; + + // Rectángulo B (Existente) + const bMinX = room.tile.x; + const bMaxX = room.tile.x + tileDef.width; + const bMinY = room.tile.y; + const bMaxY = room.tile.y + tileDef.height; + + // Check Overlap (Intersección de AABB) + // No hay colisión si alguno está totalmente a un lado del otro + const noOverlap = aMaxX <= bMinX || aMinX >= bMaxX || aMaxY <= bMinY || aMinY >= bMaxY; + + if (!noOverlap) { + console.log(`Collision detected with Room ${room.id} [${bMinX},${bMinY},${bMaxX},${bMaxY}] vs New [${aMinX},${aMinY},${aMaxX},${aMaxY}]`); + return false; + } + } + return true; +} + // Verificar si una posición es una puerta function isPositionDoor(x, y, room) { for (const door of room.doors) { @@ -683,37 +721,67 @@ function confirmOpenDoor() { if (targetDoor && originRoom) { console.log("Abriendo puerta:", targetDoor.id); + const originalDoorId = targetDoor.id; // Guardar ID original para buscar el mesh luego 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; + // Revelar sala destino (o generarla si no existe) + if (!targetDoor.leadsTo) { + // FASE DE EXPLORACIÓN: Generar nueva sala + console.log("Explorando nueva zona..."); + turnManager.setPhase(PHASES.EXPLORATION); + const newRoom = exploreRoom(originRoom, targetDoor); + if (newRoom) { + targetDoor.leadsTo = newRoom.id; + targetDoor.id = `door_${originRoom.id}_to_${newRoom.id}`; // Update ID - // 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; - } + // Actualizar también la puerta inversa en la nueva sala + const oppDoor = newRoom.doors.find(d => d.leadsTo === originRoom.id); + if (oppDoor) { + oppDoor.isOpen = true; } - } + } else { + console.log("No se pudo generar sala (bloqueado)"); + // Feedback visual de bloqueo + const toast = document.createElement('div'); + toast.style.position = 'absolute'; + toast.style.top = '50%'; + toast.style.left = '50%'; + toast.style.transform = 'translate(-50%, -50%)'; + toast.style.background = 'rgba(100, 0, 0, 0.8)'; + toast.style.color = 'white'; + toast.style.padding = '20px'; + toast.style.border = '2px solid red'; + toast.style.fontSize = '20px'; + toast.innerText = "¡CAMINO BLOQUEADO!"; + document.body.appendChild(toast); + setTimeout(() => toast.remove(), 2000); - // Revelar sala destino - if (!ROOMS.visitedRooms.has(targetRoom.id)) { - ROOMS.visitedRooms.add(targetRoom.id); - renderRoom(targetRoom); + targetDoor.isOpen = false; // Mantener cerrada + targetDoor.isBlocked = true; // Marcar como bloqueada permanentemente + + // Resetear estado + SESSION.selectedDoorId = null; + updateSelectionVisuals(); + closeDoorModal(); + return; // Cancelar apertura } } + const targetRoom = ROOMS.rooms.find(r => r.id === targetDoor.leadsTo); + + // Revelar sala destino + if (targetRoom && !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 + // Buscamos el mesh en roomMeshes usando el ID ORIGINAL (porque el del objeto puede haber cambiado) if (SESSION.roomMeshes[originRoom.id]) { - const doorMesh = SESSION.roomMeshes[originRoom.id].doors.find(m => m.userData.id === targetDoor.id); + const doorMesh = SESSION.roomMeshes[originRoom.id].doors.find(m => m.userData.id === originalDoorId); if (doorMesh) { doorMesh.visible = false; // "Abrir" visualmente desapareciendo + doorMesh.userData.id = targetDoor.id; // Sincronizar ID del mesh con el nuevo ID } } @@ -744,7 +812,23 @@ function checkDoorInteraction(unit) { // Verificar adyacencia if (isAdjacent({ x: unit.x, y: unit.y }, doorPos)) { - openDoorModal(); + if (targetDoor.isBlocked) { + // Mostrar aviso de bloqueo + const toast = document.createElement('div'); + toast.style.position = 'absolute'; + toast.style.top = '50%'; + toast.style.left = '50%'; + toast.style.transform = 'translate(-50%, -50%)'; + toast.style.background = 'rgba(100, 0, 0, 0.8)'; + toast.style.color = 'white'; + toast.style.padding = '10px'; + toast.style.border = '1px solid red'; + toast.innerText = "¡Puerta bloqueada!"; + document.body.appendChild(toast); + setTimeout(() => toast.remove(), 1000); + } else { + openDoorModal(); + } } } } @@ -1049,10 +1133,14 @@ async function renderRoom(room) { tileMesh.receiveShadow = true; const originPos = gridToWorld(room.tile.x, room.tile.y); + console.log(`[DEBUG] renderRoom ${room.id} | Tile:`, room.tile, `| WorldOrigin:`, originPos); + tileMesh.position.x = originPos.x + (worldWidth / 2) - (CONFIG.CELL_SIZE / 2); tileMesh.position.z = originPos.z + (worldHeight / 2) - (CONFIG.CELL_SIZE / 2); tileMesh.position.y = 0; + console.log(`[DEBUG] renderRoom ${room.id} | MeshPos:`, tileMesh.position); + scene.add(tileMesh); roomMeshes.tile = tileMesh; @@ -1184,12 +1272,8 @@ 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 - } + // Renderizar puerta independientemente de si tiene destino conocido o no + // (Las puertas leadsTo: null son zonas inexploradas) const doorGeometry = new THREE.PlaneGeometry(doorWidth, doorHeight); const doorMaterial = new THREE.MeshStandardMaterial({ diff --git a/src/systems/TurnManager.js b/src/systems/TurnManager.js new file mode 100644 index 0000000..8478ef8 --- /dev/null +++ b/src/systems/TurnManager.js @@ -0,0 +1,68 @@ +export const PHASES = { + POWER: 'POWER', // Roll for events + HERO: 'HERO', // Player actions + EXPLORATION: 'EXPLORATION', // Room reveal + MONSTER: 'MONSTER', // AI actions + END: 'END' // Cleanup +}; + +class TurnManager extends EventTarget { + constructor() { + super(); + this.currentTurn = 1; + this.currentPhase = PHASES.POWER; + this.isCombat = false; + } + + startTurn() { + this.currentPhase = PHASES.POWER; + this.dispatchEvent(new CustomEvent('phaseChange', { detail: this.currentPhase })); + console.log(`--- TURN ${this.currentTurn} START ---`); + + // Simulating Power Phase trigger + this.processPowerPhase(); + } + + processPowerPhase() { + // Winds of Magic Roll (1d6) + const roll = Math.floor(Math.random() * 6) + 1; + console.log(`Winds of Magic Roll: ${roll}`); + + let message = `Vientos de Magia: ${roll}`; + this.dispatchEvent(new CustomEvent('message', { detail: message })); + + if (roll === 1) { + console.log("EVENTO INESPERADO!"); + this.dispatchEvent(new CustomEvent('eventTriggered', { + detail: { + source: 'POWER_PHASE', + event: { title: 'Evento Inesperado', description: 'Algo se mueve en la oscuridad...', type: 'MISTERIO' } + } + })); + // In a real game, we'd draw from a specific Event Deck here + } else { + console.log("¡Poder obtenido!"); + } + + // Auto-advance to Hero Phase after a brief pause to show the roll + setTimeout(() => this.setPhase(PHASES.HERO), 2000); + } + + setPhase(phase) { + this.currentPhase = phase; + this.dispatchEvent(new CustomEvent('phaseChange', { detail: this.currentPhase })); + console.log(`Phase changed to: ${phase}`); + + if (phase === PHASES.END) { + this.endTurn(); + } + } + + endTurn() { + console.log(`--- TURN ${this.currentTurn} END ---`); + this.currentTurn++; + this.startTurn(); + } +} + +export const turnManager = new TurnManager();