Feat: Implement Event System, Exploration Phase, and Collision Detection

This commit is contained in:
2025-12-28 22:38:45 +01:00
parent b6ca14dfa2
commit 83dc2b0234
4 changed files with 463 additions and 222 deletions

View File

@@ -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();

41
src/dungeon/EventDeck.js Normal file
View File

@@ -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();

View File

@@ -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 = `
<h2 style="margin:0">${event.title}</h2>
<p style="margin:10px 0">${event.description}</p>
<small>${event.type}</small>
`;
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({

View File

@@ -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();