diff --git a/DEVLOG.md b/DEVLOG.md index 640bcd5..03296b3 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -2,6 +2,47 @@ Este documento sirve para llevar un control diario del desarrollo, decisiones técnicas y nuevas funcionalidades implementadas en el proyecto. +## [2025-12-29] - Sistema Avanzado de Mapeo de Tiles + +### Funcionalidades Implementadas +- **TileDefinitions.js:** Nuevo módulo centralizado con definiciones de todas las tiles (rooms, corridors, L-shapes, T-junctions). + - Cada tile incluye: dimensiones, tipo, imagen, matriz de walkability, y exits. + - Matriz de walkability: 0 = no pisable, 1-8 = pisable con capa/altura, 9 = escaleras. + +- **Sistema de Deck Abstracto:** + - El deck ahora contiene tipos abstractos (e.g., 'L', 'corridor') en lugar de tiles específicas. + - Composición: 8 rooms 4x4, 4 rooms 4x6, 12 corridors, 10 L-shapes, 8 T-junctions. + - Cuando se dibuja un tipo, el sistema selecciona aleatoriamente entre las variantes que encajan. + +- **Validación de Conexiones:** + - `canConnectTiles()`: Verifica compatibilidad de tipos, dirección de salidas, y alineación de walkability. + - Reglas de conexión: Rooms ↔ Rooms/Corridors, Corridors ↔ Rooms/Corridors/L/T, L/T ↔ Corridors. + - Validación de dirección: Si sales por N, la nueva tile debe tener salida S. + +- **Alineación de Walkability:** + - `validateWalkabilityAlignment()`: Maneja tiles de diferentes tamaños (corridor 2x6 vs room 4x4). + - Prueba offset 0 primero, luego offset 2 (ancho del corridor) si es necesario. + - Sistema de offset para desplazar L-shapes y T-junctions y alinear áreas pisables. + +- **Filtrado de Orientación:** + - Corridors se filtran por orientación: puertas E/W requieren corridors EW, puertas N/S requieren corridors NS. + - Selección exhaustiva: Cuando se dibuja una L o T, se prueban todas las variantes antes de descartar. + +### Cambios Técnicos +- Modificado `DungeonDecks.js` para usar sistema de deck abstracto. +- Actualizado `exploreRoom()` en `main.js` para trabajar con tipos abstractos y seleccionar variantes concretas. +- Nuevas funciones: `validateWalkabilityAlignment()`, `canConnectTiles()`, `getEdgeCells()`, `shouldPlaceDoor()`. +- Actualizado `renderRoom()` para usar `room.tileDef` en lugar de `ASSETS.tiles`. + +### Problemas Conocidos +- **Offset de L/T:** La alineación de L-shapes y T-junctions todavía presenta desplazamientos incorrectos en algunos casos. +- **Frecuencia de L/T:** Aunque se aumentó la cantidad en el deck, las L y T solo aparecen cuando se conectan desde corridors, limitando su frecuencia. + +### Próximos Pasos +- Depurar y corregir el cálculo del offset para L-shapes y T-junctions. +- Revisar la lógica de aplicación del offset según la dirección de conexión (N/S vs E/W). +- Considerar ajustar las reglas de conexión para permitir más variedad en la generación. + ## [2025-12-28] - Fase 1: Arquitectura Híbrida y Servidor ### Infraestructura diff --git a/assets/images/dungeons/L_NE.png b/assets/images/dungeons/L_NE.png new file mode 100644 index 0000000..1afce1a Binary files /dev/null and b/assets/images/dungeons/L_NE.png differ diff --git a/assets/images/dungeons/L_SE.png b/assets/images/dungeons/L_SE.png new file mode 100644 index 0000000..e490d33 Binary files /dev/null and b/assets/images/dungeons/L_SE.png differ diff --git a/assets/images/dungeons/L_WN.png b/assets/images/dungeons/L_WN.png new file mode 100644 index 0000000..9c42d97 Binary files /dev/null and b/assets/images/dungeons/L_WN.png differ diff --git a/assets/images/dungeons/L_WS.png b/assets/images/dungeons/L_WS.png new file mode 100644 index 0000000..eeab863 Binary files /dev/null and b/assets/images/dungeons/L_WS.png differ diff --git a/assets/images/dungeons/T_NES.png b/assets/images/dungeons/T_NES.png new file mode 100644 index 0000000..f016d47 Binary files /dev/null and b/assets/images/dungeons/T_NES.png differ diff --git a/assets/images/dungeons/T_WNE.png b/assets/images/dungeons/T_WNE.png new file mode 100644 index 0000000..b7341b5 Binary files /dev/null and b/assets/images/dungeons/T_WNE.png differ diff --git a/assets/images/dungeons/T_WNS.png b/assets/images/dungeons/T_WNS.png new file mode 100644 index 0000000..dd9ccc2 Binary files /dev/null and b/assets/images/dungeons/T_WNS.png differ diff --git a/assets/images/dungeons/T_WSE.png b/assets/images/dungeons/T_WSE.png new file mode 100644 index 0000000..2abcf28 Binary files /dev/null and b/assets/images/dungeons/T_WSE.png differ diff --git a/assets/images/dungeons/corridor1_EW.png b/assets/images/dungeons/corridor1_EW.png new file mode 100644 index 0000000..65a8830 Binary files /dev/null and b/assets/images/dungeons/corridor1_EW.png differ diff --git a/assets/images/dungeons/corridor1_NS.png b/assets/images/dungeons/corridor1_NS.png new file mode 100644 index 0000000..4ffcbf7 Binary files /dev/null and b/assets/images/dungeons/corridor1_NS.png differ diff --git a/assets/images/dungeons/corridor2_EW.png b/assets/images/dungeons/corridor2_EW.png new file mode 100644 index 0000000..0d62bfc Binary files /dev/null and b/assets/images/dungeons/corridor2_EW.png differ diff --git a/assets/images/dungeons/corridor2_NS.png b/assets/images/dungeons/corridor2_NS.png new file mode 100644 index 0000000..5f2932a Binary files /dev/null and b/assets/images/dungeons/corridor2_NS.png differ diff --git a/assets/images/dungeons/corridor3_EW.png b/assets/images/dungeons/corridor3_EW.png new file mode 100644 index 0000000..01c2e1b Binary files /dev/null and b/assets/images/dungeons/corridor3_EW.png differ diff --git a/assets/images/dungeons/corridor3_NS.png b/assets/images/dungeons/corridor3_NS.png new file mode 100644 index 0000000..f03f47d Binary files /dev/null and b/assets/images/dungeons/corridor3_NS.png differ diff --git a/assets/images/dungeons/room_4x4_circle.png b/assets/images/dungeons/room_4x4_circle.png new file mode 100644 index 0000000..8f7d08f Binary files /dev/null and b/assets/images/dungeons/room_4x4_circle.png differ diff --git a/assets/images/dungeons/room_4x4_normal.png b/assets/images/dungeons/room_4x4_normal.png new file mode 100644 index 0000000..48f2438 Binary files /dev/null and b/assets/images/dungeons/room_4x4_normal.png differ diff --git a/assets/images/dungeons/room_4x4_squeleton.png b/assets/images/dungeons/room_4x4_squeleton.png new file mode 100644 index 0000000..91f7acd Binary files /dev/null and b/assets/images/dungeons/room_4x4_squeleton.png differ diff --git a/assets/images/dungeons/room_4x6_altar.png b/assets/images/dungeons/room_4x6_altar.png new file mode 100644 index 0000000..37a69f6 Binary files /dev/null and b/assets/images/dungeons/room_4x6_altar.png differ diff --git a/assets/images/dungeons/room_4x6_tomb.png b/assets/images/dungeons/room_4x6_tomb.png new file mode 100644 index 0000000..80f1e86 Binary files /dev/null and b/assets/images/dungeons/room_4x6_tomb.png differ diff --git a/src/dungeon/DungeonDecks.js b/src/dungeon/DungeonDecks.js index dd44138..9190a9e 100644 --- a/src/dungeon/DungeonDecks.js +++ b/src/dungeon/DungeonDecks.js @@ -1,9 +1,11 @@ -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" } -]; +import { ROOMS, CORRIDORS, L_SHAPES, T_JUNCTIONS } from './TileDefinitions.js'; + +/** + * DungeonDeck - Manages the deck of dungeon tiles + * + * New approach: Deck contains abstract tile types, not specific tiles + * When a type is drawn, we select a fitting variant from available options + */ export class DungeonDeck { constructor() { @@ -13,10 +15,59 @@ export class DungeonDeck { } shuffleDeck() { - // Create a new deck with multiple copies of cards + // Create a deck with abstract tile types 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 + // 8 rooms 4x4 (abstract) + { type: 'room_4x4' }, + { type: 'room_4x4' }, + { type: 'room_4x4' }, + { type: 'room_4x4' }, + { type: 'room_4x4' }, + { type: 'room_4x4' }, + { type: 'room_4x4' }, + { type: 'room_4x4' }, + + // 4 rooms 4x6 (abstract) + { type: 'room_4x6' }, + { type: 'room_4x6' }, + { type: 'room_4x6' }, + { type: 'room_4x6' }, + + // 12 corridors (abstract) + { type: 'corridor' }, + { type: 'corridor' }, + { type: 'corridor' }, + { type: 'corridor' }, + { type: 'corridor' }, + { type: 'corridor' }, + { type: 'corridor' }, + { type: 'corridor' }, + { type: 'corridor' }, + { type: 'corridor' }, + { type: 'corridor' }, + { type: 'corridor' }, + + // 10 L-shapes (abstract) - increased from 6 + { type: 'L' }, + { type: 'L' }, + { type: 'L' }, + { type: 'L' }, + { type: 'L' }, + { type: 'L' }, + { type: 'L' }, + { type: 'L' }, + { type: 'L' }, + { type: 'L' }, + + // 8 T-junctions (abstract) - increased from 4 + { type: 'T' }, + { type: 'T' }, + { type: 'T' }, + { type: 'T' }, + { type: 'T' }, + { type: 'T' }, + { type: 'T' }, + { type: 'T' } ]; // Fisher-Yates shuffle @@ -43,6 +94,48 @@ export class DungeonDeck { this.discardPile.push(card); return card; } + + /** + * Draw a compatible abstract tile type + * @param {string} originTileType - Type of the origin tile ('room', 'corridor', 'L', 'T') + * @param {number} maxAttempts - Maximum number of cards to try + * @returns {object|null} - Abstract tile card or null if none found + */ + drawCompatibleCard(originTileType, maxAttempts = 10) { + const validTypes = this.getCompatibleTypes(originTileType); + + for (let i = 0; i < maxAttempts && this.deck.length > 0; i++) { + const card = this.drawCard(); + + // Check if this abstract type is compatible + if (validTypes.includes(card.type)) { + return card; + } + + // Put incompatible card back at the bottom of the deck + this.deck.unshift(card); + this.discardPile.pop(); + } + + console.warn(`Could not find compatible tile for ${originTileType} after ${maxAttempts} attempts`); + return null; + } + + /** + * Get compatible tile types for a given origin type + * @param {string} originType - Type of the origin tile + * @returns {string[]} - Array of compatible abstract tile types + */ + getCompatibleTypes(originType) { + const connectionRules = { + 'room': ['room_4x4', 'room_4x6', 'corridor'], + 'corridor': ['room_4x4', 'room_4x6', 'corridor', 'L', 'T'], + 'L': ['corridor'], + 'T': ['corridor'] + }; + + return connectionRules[originType] || []; + } } export const dungeonDeck = new DungeonDeck(); diff --git a/src/dungeon/TileDefinitions.js b/src/dungeon/TileDefinitions.js new file mode 100644 index 0000000..95eef71 --- /dev/null +++ b/src/dungeon/TileDefinitions.js @@ -0,0 +1,431 @@ +/** + * TileDefinitions.js + * + * Defines all dungeon tiles with their properties: + * - Dimensions (width x height in cells) + * - Walkability matrix (0 = not walkable, 1-8 = walkable layer/height, 9 = stairs) + * - Tile type (room, corridor, L, T) + * - Exit points + * - Image path + */ + +// ============================================================================ +// ROOMS (4x4 and 4x6) +// ============================================================================ + +const ROOM_4X4_NORMAL = { + id: 'room_4x4_normal', + tileType: 'room', + width: 4, + height: 4, + image: '/assets/images/dungeons/room_4x4_normal.png', + walkability: [ + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1] + ], + exits: ['N', 'S', 'E', 'W'] +}; + +const ROOM_4X4_CIRCLE = { + id: 'room_4x4_circle', + tileType: 'room', + width: 4, + height: 4, + image: '/assets/images/dungeons/room_4x4_circle.png', + walkability: [ + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1] + ], + exits: ['N', 'S', 'E', 'W'] +}; + +const ROOM_4X4_SKELETON = { + id: 'room_4x4_skeleton', + tileType: 'room', + width: 4, + height: 4, + image: '/assets/images/dungeons/room_4x4_squeleton.png', + walkability: [ + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1] + ], + exits: ['N', 'S', 'E', 'W'] +}; + +const ROOM_4X6_ALTAR = { + id: 'room_4x6_altar', + tileType: 'room', + width: 4, + height: 6, + image: '/assets/images/dungeons/room_4x6_altar.png', + walkability: [ + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1] + ], + exits: ['N', 'S', 'E', 'W'] +}; + +const ROOM_4X6_TOMB = { + id: 'room_4x6_tomb', + tileType: 'room', + width: 4, + height: 6, + image: '/assets/images/dungeons/room_4x6_tomb.png', + walkability: [ + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1] + ], + exits: ['N', 'S', 'E', 'W'] +}; + +// Example room with stairs (2 levels) +const ROOM_4X6_STAIRS = { + id: 'room_4x6_stairs', + tileType: 'room', + width: 4, + height: 6, + image: '/assets/images/dungeons/room_4x6_altar.png', // Using altar image as placeholder + walkability: [ + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 9, 9, 1], // Stairs connecting level 1 and 2 + [1, 2, 2, 1], + [2, 2, 2, 2], + [2, 2, 2, 2] + ], + exits: ['N', 'S'] +}; + +// ============================================================================ +// L-SHAPES (4x4 with 2-tile rows) +// ============================================================================ + +const L_NE = { + id: 'L_NE', + tileType: 'L', + width: 4, + height: 4, + image: '/assets/images/dungeons/L_NE.png', + walkability: [ + [1, 1, 0, 0], + [1, 1, 0, 0], + [1, 1, 1, 1], + [1, 1, 1, 1] + ], + exits: ['N', 'E'] +}; + +const L_SE = { + id: 'L_SE', + tileType: 'L', + width: 4, + height: 4, + image: '/assets/images/dungeons/L_SE.png', + walkability: [ + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 0, 0], + [1, 1, 0, 0] + ], + exits: ['S', 'E'] +}; + +const L_WS = { + id: 'L_WS', + tileType: 'L', + width: 4, + height: 4, + image: '/assets/images/dungeons/L_WS.png', + walkability: [ + [1, 1, 1, 1], + [1, 1, 1, 1], + [0, 0, 1, 1], + [0, 0, 1, 1] + ], + exits: ['W', 'S'] +}; + +const L_WN = { + id: 'L_WN', + tileType: 'L', + width: 4, + height: 4, + image: '/assets/images/dungeons/L_WN.png', + walkability: [ + [0, 0, 1, 1], + [0, 0, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1] + ], + exits: ['W', 'N'] +}; + +// ============================================================================ +// T-JUNCTIONS (4x6 or 6x4 with 2-tile rows) +// ============================================================================ + +const T_NES = { + id: 'T_NES', + tileType: 'T', + width: 4, + height: 6, + image: '/assets/images/dungeons/T_NES.png', + walkability: [ + [1, 1, 0, 0], + [1, 1, 0, 0], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 0, 0], + [1, 1, 0, 0] + ], + exits: ['N', 'E', 'S'] +}; + +const T_WNE = { + id: 'T_WNE', + tileType: 'T', + width: 6, + height: 4, + image: '/assets/images/dungeons/T_WNE.png', + walkability: [ + [0, 0, 1, 1, 0, 0], + [0, 0, 1, 1, 0, 0], + [1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1] + ], + exits: ['W', 'N', 'E'] +}; + +const T_WSE = { + id: 'T_WSE', + tileType: 'T', + width: 6, + height: 4, + image: '/assets/images/dungeons/T_WSE.png', + walkability: [ + [1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1], + [0, 0, 1, 1, 0, 0], + [0, 0, 1, 1, 0, 0] + ], + exits: ['W', 'S', 'E'] +}; + +const T_WNS = { + id: 'T_WNS', + tileType: 'T', + width: 4, + height: 6, + image: '/assets/images/dungeons/T_WNS.png', + walkability: [ + [0, 0, 1, 1], + [0, 0, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [0, 0, 1, 1], + [0, 0, 1, 1] + ], + exits: ['W', 'N', 'S'] +}; + +// ============================================================================ +// CORRIDORS (2x6 or 6x2 with 2-tile rows) +// ============================================================================ + +// Corridor 1 - East-West (horizontal) +const CORRIDOR1_EW = { + id: 'corridor1_EW', + tileType: 'corridor', + width: 6, + height: 2, + image: '/assets/images/dungeons/corridor1_EW.png', + walkability: [ + [1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1] + ], + exits: ['E', 'W'] +}; + +// Corridor 1 - North-South (vertical) +const CORRIDOR1_NS = { + id: 'corridor1_NS', + tileType: 'corridor', + width: 2, + height: 6, + image: '/assets/images/dungeons/corridor1_NS.png', + walkability: [ + [1, 1], + [1, 1], + [1, 1], + [1, 1], + [1, 1], + [1, 1] + ], + exits: ['N', 'S'] +}; + +// Corridor 2 - East-West (horizontal) +const CORRIDOR2_EW = { + id: 'corridor2_EW', + tileType: 'corridor', + width: 6, + height: 2, + image: '/assets/images/dungeons/corridor2_EW.png', + walkability: [ + [1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1] + ], + exits: ['E', 'W'] +}; + +// Corridor 2 - North-South (vertical) +const CORRIDOR2_NS = { + id: 'corridor2_NS', + tileType: 'corridor', + width: 2, + height: 6, + image: '/assets/images/dungeons/corridor2_NS.png', + walkability: [ + [1, 1], + [1, 1], + [1, 1], + [1, 1], + [1, 1], + [1, 1] + ], + exits: ['N', 'S'] +}; + +// Corridor 3 - East-West (horizontal) +const CORRIDOR3_EW = { + id: 'corridor3_EW', + tileType: 'corridor', + width: 6, + height: 2, + image: '/assets/images/dungeons/corridor3_EW.png', + walkability: [ + [1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1] + ], + exits: ['E', 'W'] +}; + +// Corridor 3 - North-South (vertical) +const CORRIDOR3_NS = { + id: 'corridor3_NS', + tileType: 'corridor', + width: 2, + height: 6, + image: '/assets/images/dungeons/corridor3_NS.png', + walkability: [ + [1, 1], + [1, 1], + [1, 1], + [1, 1], + [1, 1], + [1, 1] + ], + exits: ['N', 'S'] +}; + +// ============================================================================ +// TILE COLLECTIONS +// ============================================================================ + +export const TILE_DEFINITIONS = { + // Rooms + room_4x4_normal: ROOM_4X4_NORMAL, + room_4x4_circle: ROOM_4X4_CIRCLE, + room_4x4_skeleton: ROOM_4X4_SKELETON, + room_4x6_altar: ROOM_4X6_ALTAR, + room_4x6_tomb: ROOM_4X6_TOMB, + room_4x6_stairs: ROOM_4X6_STAIRS, + + // L-shapes + L_NE: L_NE, + L_SE: L_SE, + L_WS: L_WS, + L_WN: L_WN, + + // T-junctions + T_NES: T_NES, + T_WNE: T_WNE, + T_WSE: T_WSE, + T_WNS: T_WNS, + + // Corridors + corridor1_EW: CORRIDOR1_EW, + corridor1_NS: CORRIDOR1_NS, + corridor2_EW: CORRIDOR2_EW, + corridor2_NS: CORRIDOR2_NS, + corridor3_EW: CORRIDOR3_EW, + corridor3_NS: CORRIDOR3_NS +}; + +// Collections by type for easy filtering +export const ROOMS = [ + ROOM_4X4_NORMAL, + ROOM_4X4_CIRCLE, + ROOM_4X4_SKELETON, + ROOM_4X6_ALTAR, + ROOM_4X6_TOMB, + ROOM_4X6_STAIRS +]; + +export const L_SHAPES = [ + L_NE, + L_SE, + L_WS, + L_WN +]; + +export const T_JUNCTIONS = [ + T_NES, + T_WNE, + T_WSE, + T_WNS +]; + +export const CORRIDORS = [ + CORRIDOR1_EW, + CORRIDOR1_NS, + CORRIDOR2_EW, + CORRIDOR2_NS, + CORRIDOR3_EW, + CORRIDOR3_NS +]; + +// Helper function to get tile definition by ID +export function getTileDefinition(tileId) { + return TILE_DEFINITIONS[tileId] || null; +} + +// Helper function to get tiles by type +export function getTilesByType(tileType) { + switch (tileType) { + case 'room': + return ROOMS; + case 'L': + return L_SHAPES; + case 'T': + return T_JUNCTIONS; + case 'corridor': + return CORRIDORS; + default: + return []; + } +} diff --git a/src/main.js b/src/main.js index 262be3d..eca5071 100644 --- a/src/main.js +++ b/src/main.js @@ -4,7 +4,9 @@ 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 * as TileDefinitions from './dungeon/TileDefinitions.js'; import { eventDeck } from './dungeon/EventDeck.js'; // Import Event Deck +import { TILE_DEFINITIONS, getTileDefinition } from './dungeon/TileDefinitions.js'; // --- NETWORK SETUP --- // Dynamic connection to support playing from mobile on the same network @@ -78,9 +80,11 @@ const ASSETS = { // --- GENERACIÓN DE MAZMORRA (DINÁMICA) --- function generateDungeon() { // Start with a single entry room 4x4 + const startTileDef = getTileDefinition('room_4x4_normal'); const startRoom = { id: 1, - tile: { type: 'tile_base', x: 0, y: 0 }, + tileDef: startTileDef, // Store the full tile definition + tile: { id: startTileDef.id, 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 } @@ -99,9 +103,6 @@ function generateDungeon() { // --- 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) { @@ -109,18 +110,90 @@ function exploreRoom(originRoom, door) { showUIEvent(eventCard); } - const nextTileDef = ASSETS.tiles[card.type]; - const newRoomId = ROOMS.rooms.length + 1; - - // 1. Determinar lado de entrada (Opuesto al de salida) + // Determine entry side (opposite of exit) 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 + // Try to draw a compatible abstract tile type (up to 10 attempts) + let card = null; + let alignmentOffset = 0; + let attempts = 0; + const maxAttempts = 10; + + while (attempts < maxAttempts && !card) { + const abstractCard = dungeonDeck.drawCompatibleCard(originRoom.tileDef.tileType); + if (!abstractCard) { + console.warn("Could not draw compatible card"); + return null; + } + + console.log(`Drew abstract type: ${abstractCard.type}`); + + // Select concrete tile variant based on abstract type + let candidates = []; + + switch (abstractCard.type) { + case 'room_4x4': + candidates = TileDefinitions.ROOMS.filter(r => r.width === 4 && r.height === 4); + break; + case 'room_4x6': + candidates = TileDefinitions.ROOMS.filter(r => r.width === 4 && r.height === 6); + break; + case 'corridor': + // Filter by orientation (EW or NS based on exit direction) + const isExitHorizontal = door.side === 'E' || door.side === 'W'; + candidates = TileDefinitions.CORRIDORS.filter(c => { + const isCorridorHorizontal = c.exits.includes('E') && c.exits.includes('W'); + return isExitHorizontal === isCorridorHorizontal; + }); + break; + case 'L': + candidates = [...TileDefinitions.L_SHAPES]; + break; + case 'T': + candidates = [...TileDefinitions.T_JUNCTIONS]; + break; + } + + if (candidates.length === 0) { + console.warn(`No candidates found for type ${abstractCard.type}`); + attempts++; + continue; + } + + // Try all candidates and collect those that fit + const fittingVariants = []; + for (const variant of candidates) { + const connectionResult = canConnectTiles(originRoom, variant, door.side); + if (connectionResult.valid) { + fittingVariants.push({ variant, offset: connectionResult.offset }); + } + } + + if (fittingVariants.length > 0) { + // RANDOM selection from fitting variants + const selected = fittingVariants[Math.floor(Math.random() * fittingVariants.length)]; + card = selected.variant; + alignmentOffset = selected.offset; + console.log(`✓ Selected ${card.id} (${card.tileType}) randomly from ${fittingVariants.length} fitting variants, offset ${alignmentOffset}`); + } else { + console.log(`✗ No ${abstractCard.type} variant fits, trying another tile type...`); + attempts++; + } + } + + if (!card) { + console.error("Could not find valid tile after", maxAttempts, "attempts"); + return null; + } + + const nextTileDef = card; + const newRoomId = ROOMS.rooms.length + 1; + + // Calculate entry door position in the new tile let entryGridX, entryGridY; if (entrySide === 'N') { @@ -137,13 +210,20 @@ function exploreRoom(originRoom, door) { 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; + // Calculate absolute position for the new tile + let newX = originRoom.tile.x + door.gridX - entryGridX; + let newY = originRoom.tile.y + door.gridY - entryGridY; - // Comprobar colisiones + // Apply alignment offset based on exit direction + if (door.side === 'N' || door.side === 'S') { + // Vertical connection - offset horizontally + newX += alignmentOffset; + } else { + // Horizontal connection - offset vertically + newY += alignmentOffset; + } + + // Check for collisions if (!isAreaFree(newX, newY, nextTileDef.width, nextTileDef.height)) { console.warn("Cannot place room: Collision detected!"); return null; @@ -151,17 +231,22 @@ function exploreRoom(originRoom, door) { const newRoom = { id: newRoomId, - tile: { type: card.type, x: newX, y: newY }, + tileDef: nextTileDef, + tile: { id: nextTileDef.id, x: newX, y: newY }, walls: ['N', 'S', 'E', 'W'], doors: [], entities: [] }; - // Crear la puerta de entrada + // Determine if we should place a door or open connection + const placeDoor = shouldPlaceDoor(originRoom.tileDef.tileType, nextTileDef.tileType); + + // Create the entry door/connection const entryDoor = { side: entrySide, leadsTo: originRoom.id, - isOpen: true, + isOpen: !placeDoor, // Open if no door, closed if door + isDoor: placeDoor, id: `door_${newRoomId}_to_${originRoom.id}`, gridX: entryGridX, gridY: entryGridY @@ -169,18 +254,19 @@ function exploreRoom(originRoom, door) { newRoom.doors.push(entryDoor); - // Generar salidas adicionales según la carta - card.exits.forEach(exitDir => { - if (exitDir === entrySide) return; // Ya tenemos esta puerta + // Generate additional exits based on the tile definition + nextTileDef.exits.forEach(exitDir => { + if (exitDir === entrySide) return; // Already have this connection const exitDoor = { side: exitDir, - leadsTo: null, // Desconocido + leadsTo: null, isOpen: false, + isDoor: true, // Will be determined when connected id: `door_${newRoomId}_${exitDir}` }; - // Calcular coordenadas de la puerta en la nueva sala + // Calculate door coordinates if (exitDir === 'N') { exitDoor.gridX = Math.floor(nextTileDef.width / 2); exitDoor.gridY = 0; @@ -199,6 +285,7 @@ function exploreRoom(originRoom, door) { }); ROOMS.rooms.push(newRoom); + console.log(`✓ Tile ${newRoomId} (${nextTileDef.tileType}) created: ${nextTileDef.id} at (${newX}, ${newY})`); return newRoom; } @@ -514,7 +601,9 @@ function isAdjacent(p1, p2) { // Verificar si una posición está dentro de una sala function isPositionInRoom(x, y, room) { const tile = room.tile; - const tileDef = ASSETS.tiles[tile.type]; + const tileDef = room.tileDef; + if (!tileDef) return false; + const minX = tile.x; const maxX = tile.x + tileDef.width - 1; const minY = tile.y; @@ -527,16 +616,14 @@ function isPositionInRoom(x, y, room) { // 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]; + const tileDef = room.tileDef; + if (!tileDef) continue; // Rectángulo B (Existente) const bMinX = room.tile.x; @@ -545,7 +632,6 @@ function isAreaFree(x, y, width, height) { 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) { @@ -568,14 +654,21 @@ function isPositionDoor(x, y, room) { } -// Verificar si una celda es transitable (bloquear puertas cerradas) + +// Verificar si una celda es transitable usando la matriz de walkability function isWalkable(x, y) { for (const roomId of ROOMS.visitedRooms) { const room = ROOMS.rooms.find(r => r.id === roomId); - if (!room) continue; + if (!room || !room.tileDef) continue; if (isPositionInRoom(x, y, room)) { - return true; + // Get local coordinates within the tile + const localX = x - room.tile.x; + const localY = y - room.tile.y; + + // Check walkability matrix + const walkValue = room.tileDef.walkability[localY][localX]; + return walkValue > 0; // 0 = not walkable, >0 = walkable } // Verificar puertas @@ -589,6 +682,254 @@ function isWalkable(x, y) { return false; } +// Get the layer/height of a specific position +function getTileLayer(x, y) { + for (const roomId of ROOMS.visitedRooms) { + const room = ROOMS.rooms.find(r => r.id === roomId); + if (!room || !room.tileDef) continue; + + if (isPositionInRoom(x, y, room)) { + const localX = x - room.tile.x; + const localY = y - room.tile.y; + + return room.tileDef.walkability[localY][localX]; + } + } + return 0; // Not in any room +} + +// Check if movement between two positions with different layers is allowed +function canTransitionLayers(fromX, fromY, toX, toY) { + const fromLayer = getTileLayer(fromX, fromY); + const toLayer = getTileLayer(toX, toY); + + // Same layer or one is a stair + if (fromLayer === toLayer || fromLayer === 9 || toLayer === 9) { + return true; + } + + // Different layers - check if there's a stair adjacent + const layerDiff = Math.abs(fromLayer - toLayer); + if (layerDiff > 1) { + return false; // Can't jump more than 1 layer + } + + // Check if there's a stair (9) adjacent to either position + const adjacentPositions = [ + { x: fromX - 1, y: fromY }, + { x: fromX + 1, y: fromY }, + { x: fromX, y: fromY - 1 }, + { x: fromX, y: fromY + 1 }, + { x: toX - 1, y: toY }, + { x: toX + 1, y: toY }, + { x: toX, y: toY - 1 }, + { x: toX, y: toY + 1 } + ]; + + for (const pos of adjacentPositions) { + if (getTileLayer(pos.x, pos.y) === 9) { + return true; // Found a stair + } + } + + return false; // No stair found +} + +// Determine if a door should be placed between two tile types +function shouldPlaceDoor(tileTypeA, tileTypeB) { + // Doors only between Room ↔ Corridor + return (tileTypeA === 'room' && tileTypeB === 'corridor') || + (tileTypeA === 'corridor' && tileTypeB === 'room'); +} + +// Validate walkability alignment between two tiles at their connection point +// Returns: { valid: boolean, offset: number } - offset is how much to shift tileB to align with tileA +function validateWalkabilityAlignment(tileDefA, posA, tileDefB, posB, exitSide) { + // Get the edge cells that will connect + const edgeA = getEdgeCells(tileDefA, exitSide); + const edgeB = getEdgeCells(tileDefB, getOppositeSide(exitSide)); + + console.log(`[ALIGN] Checking ${tileDefA.id} (${exitSide}) → ${tileDefB.id}`); + console.log(`[ALIGN] EdgeA (${tileDefA.id}):`, edgeA); + console.log(`[ALIGN] EdgeB (${tileDefB.id}):`, edgeB); + + // Special handling for corridor connections + // Corridors are 2 tiles wide, rooms/L/T are typically 4 tiles wide + // We need to find where the corridor's walkable area aligns with the room's walkable area + + if (edgeA.length !== edgeB.length) { + // Different edge lengths - need to find alignment + const smallerEdge = edgeA.length < edgeB.length ? edgeA : edgeB; + const largerEdge = edgeA.length < edgeB.length ? edgeB : edgeA; + const isASmaller = edgeA.length < edgeB.length; + + console.log(`[ALIGN] Different sizes: ${edgeA.length} vs ${edgeB.length}`); + console.log(`[ALIGN] isASmaller: ${isASmaller}`); + + // Find walkable cells in smaller edge + const smallerWalkable = smallerEdge.filter(cell => cell > 0); + if (smallerWalkable.length === 0) { + console.warn('[ALIGN] No walkable cells in smaller edge'); + return { valid: false, offset: 0 }; + } + + const smallerWidth = smallerEdge.length; + const largerWidth = largerEdge.length; + + // FIRST: Try offset 0 (no displacement needed) + let validAtZero = true; + for (let i = 0; i < smallerWidth; i++) { + const smallCell = smallerEdge[i]; + const largeCell = largerEdge[i]; + + const isSmallWalkable = smallCell > 0; + const isLargeWalkable = largeCell > 0; + + console.log(`[ALIGN] Offset 0, index ${i}: small=${smallCell}(${isSmallWalkable}) vs large=${largeCell}(${isLargeWalkable})`); + + if (isSmallWalkable !== isLargeWalkable) { + validAtZero = false; + console.log(`[ALIGN] ❌ Offset 0 FAILED at index ${i}: walkability mismatch`); + break; + } + } + + if (validAtZero) { + console.log(`✓ [ALIGN] Valid alignment at offset 0 (no displacement needed)`); + return { valid: true, offset: 0 }; + } + + // If offset 0 doesn't work, try offset of 2 (corridor width) + // This aligns the corridor with the other walkable section of the L/T + const offset = 2; + if (offset <= largerWidth - smallerWidth) { + let valid = true; + for (let i = 0; i < smallerWidth; i++) { + const smallCell = smallerEdge[i]; + const largeCell = largerEdge[offset + i]; + + const isSmallWalkable = smallCell > 0; + const isLargeWalkable = largeCell > 0; + + if (isSmallWalkable !== isLargeWalkable) { + valid = false; + console.log(`[ALIGN] Offset ${offset} failed at index ${i}: ${smallCell} vs ${largeCell}`); + break; + } + } + + if (valid) { + // Calculate final offset + // If A (corridor) is smaller than B (L/T), we need to shift B by +offset + // If B is smaller than A, we need to shift B by -offset + const finalOffset = isASmaller ? offset : -offset; + console.log(`✓ [ALIGN] Valid alignment at offset ${offset}, final offset: ${finalOffset} (isASmaller: ${isASmaller})`); + return { valid: true, offset: finalOffset }; + } + } + + console.warn('[ALIGN] Could not find valid alignment for edges of different sizes'); + return { valid: false, offset: 0 }; + } + + // Same length - check direct alignment + for (let i = 0; i < edgeA.length; i++) { + const cellA = edgeA[i]; + const cellB = edgeB[i]; + + // Rule: Cannot connect 0 (not walkable) with >0 (walkable) + const isAWalkable = cellA > 0; + const isBWalkable = cellB > 0; + + if (isAWalkable !== isBWalkable) { + console.warn(`[ALIGN] Walkability mismatch at index ${i}: ${cellA} vs ${cellB}`); + return { valid: false, offset: 0 }; + } + } + + return { valid: true, offset: 0 }; +} + +// Get edge cells from a tile definition for a given side +function getEdgeCells(tileDef, side) { + const { walkability, width, height } = tileDef; + const cells = []; + + switch (side) { + case 'N': + // Top row + for (let x = 0; x < width; x++) { + cells.push(walkability[0][x]); + } + break; + case 'S': + // Bottom row + for (let x = 0; x < width; x++) { + cells.push(walkability[height - 1][x]); + } + break; + case 'E': + // Right column + for (let y = 0; y < height; y++) { + cells.push(walkability[y][width - 1]); + } + break; + case 'W': + // Left column + for (let y = 0; y < height; y++) { + cells.push(walkability[y][0]); + } + break; + } + + return cells; +} + +// Get opposite side +function getOppositeSide(side) { + const opposites = { 'N': 'S', 'S': 'N', 'E': 'W', 'W': 'E' }; + return opposites[side]; +} + +// Check if two tiles can connect based on type rules and walkability alignment +// Returns: { valid: boolean, offset: number } - offset for positioning the new tile +function canConnectTiles(roomA, tileDefB, exitSide) { + const tileDefA = roomA.tileDef; + + // Check type compatibility + const typeA = tileDefA.tileType; + const typeB = tileDefB.tileType; + + const validConnections = { + 'room': ['room', 'corridor'], + 'corridor': ['room', 'corridor', 'L', 'T'], + 'L': ['corridor'], + 'T': ['corridor'] + }; + + if (!validConnections[typeA] || !validConnections[typeA].includes(typeB)) { + console.warn(`Invalid connection: ${typeA} cannot connect to ${typeB}`); + return { valid: false, offset: 0 }; + } + + // CRITICAL: Check that tileB has an exit in the opposite direction + // If we exit through N, the new tile must have S in its exits + const requiredExit = getOppositeSide(exitSide); + if (!tileDefB.exits.includes(requiredExit)) { + console.warn(`Exit direction mismatch: ${tileDefB.id} doesn't have required exit '${requiredExit}' (exiting via '${exitSide}')`); + return { valid: false, offset: 0 }; + } + + // Check walkability alignment and get offset + const alignmentResult = validateWalkabilityAlignment(tileDefA, roomA.tile, tileDefB, null, exitSide); + if (!alignmentResult.valid) { + console.warn('Walkability alignment failed'); + return { valid: false, offset: 0 }; + } + + return alignmentResult; +} + // --- CREACIÓN DE MARCADORES --- function createPathMarker(stepNumber) { const canvas = document.createElement('canvas'); @@ -996,10 +1337,17 @@ function checkDoorTransition(unit, currentRoom) { } } + function getDoorGridPosition(room, door) { const tile = room.tile; - const tileWidth = ASSETS.tiles[tile.type].width; - const tileHeight = ASSETS.tiles[tile.type].height; + const tileDef = room.tileDef; + if (!tileDef) { + console.error("Room", room.id, "has no tileDef in getDoorGridPosition!"); + return { x: tile.x, y: tile.y }; + } + + const tileWidth = tileDef.width; + const tileHeight = tileDef.height; switch (door.side) { case 'N': @@ -1102,21 +1450,22 @@ async function renderRoom(room) { entities: [] }; - // Renderizar tile - // Renderizar tile - const tileDef = ASSETS.tiles[room.tile.type]; - const baseTex = await loadTexture(tileDef.src); + // Renderizar tile usando la nueva definición + const tileDef = room.tileDef; + if (!tileDef) { + console.error("Room", room.id, "has no tileDef!"); + return; + } + + const baseTex = await loadTexture(tileDef.image); 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; - // 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); + // No repetir la textura - cada tile tiene su propia imagen completa + tileTex.repeat.set(1, 1); const worldWidth = tileDef.width * CONFIG.CELL_SIZE; const worldHeight = tileDef.height * CONFIG.CELL_SIZE; @@ -1374,7 +1723,8 @@ function drawMinimap() { let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; ROOMS.rooms.forEach(room => { - const tileDef = ASSETS.tiles[room.tile.type]; + const tileDef = room.tileDef; + if (!tileDef) return; minX = Math.min(minX, room.tile.x); maxX = Math.max(maxX, room.tile.x + tileDef.width); minY = Math.min(minY, room.tile.y); @@ -1406,7 +1756,8 @@ function drawMinimap() { // 2. Dibujar TODAS las Salas ROOMS.rooms.forEach(room => { - const tileDef = ASSETS.tiles[room.tile.type]; + const tileDef = room.tileDef; + if (!tileDef) return; const pos = toCanvas(room.tile.x, room.tile.y); const w = tileDef.width * scale;