Compare commits
4 Commits
v0.6-explo
...
advanced_m
| Author | SHA1 | Date | |
|---|---|---|---|
| 8502f70bda | |||
| 75cf63f906 | |||
| 5804db396f | |||
| c8cc35772f |
47
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
|
||||
@@ -87,3 +128,9 @@ Este documento sirve para llevar un control diario del desarrollo, decisiones t
|
||||
- Definido el **Manifiesto Técnico (v2.0)**: Visión de un "Puente Híbrido" entre juego de mesa físico y motor narrativo digital (LLM).
|
||||
- **Generación Procedural:** Algoritmo de mazmorras basado en tiles de 4x4 con expansión orgánica.
|
||||
- **Motor Gráfico:** Three.js con cámara isométrica ortográfica y controles restringidos (N, S, E, W).
|
||||
|
||||
## [2025-12-29] Dungeon Generation Fixes
|
||||
- **Alignment Fix:** Implemented explicit `doorCoordinates` for all asymmetric tiles (L, T) and corridors. This replaces the generic center-based logic and ensures precise "bit-to-bit" alignment of connections, solving visual drift and 1-cell gaps.
|
||||
- **Deck Randomness Fix:** Removed a legacy debug fixed deck that was limiting the game to 7 cards.
|
||||
- **Infinite Generation:** Improved `drawCompatibleCard` to automatically reshuffle the discard pile back into the deck if the current draw pile is exhausted during a search. This prevents "Could not find compatible tile" errors.
|
||||
- **Balance Update:** Rebalanced the deck composition to increase room frequency (~50% rooms, ~50% paths).
|
||||
|
||||
BIN
assets/images/dungeons/L_NE.png
Normal file
|
After Width: | Height: | Size: 757 KiB |
BIN
assets/images/dungeons/L_SE.png
Normal file
|
After Width: | Height: | Size: 761 KiB |
BIN
assets/images/dungeons/L_WN.png
Normal file
|
After Width: | Height: | Size: 760 KiB |
BIN
assets/images/dungeons/L_WS.png
Normal file
|
After Width: | Height: | Size: 758 KiB |
BIN
assets/images/dungeons/T_NES.png
Normal file
|
After Width: | Height: | Size: 1017 KiB |
BIN
assets/images/dungeons/T_WNE.png
Normal file
|
After Width: | Height: | Size: 1013 KiB |
BIN
assets/images/dungeons/T_WNS.png
Normal file
|
After Width: | Height: | Size: 1015 KiB |
BIN
assets/images/dungeons/T_WSE.png
Normal file
|
After Width: | Height: | Size: 1012 KiB |
BIN
assets/images/dungeons/corridor1_EW.png
Normal file
|
After Width: | Height: | Size: 670 KiB |
BIN
assets/images/dungeons/corridor1_NS.png
Normal file
|
After Width: | Height: | Size: 667 KiB |
BIN
assets/images/dungeons/corridor2_EW.png
Normal file
|
After Width: | Height: | Size: 745 KiB |
BIN
assets/images/dungeons/corridor2_NS.png
Normal file
|
After Width: | Height: | Size: 741 KiB |
BIN
assets/images/dungeons/corridor3_EW.png
Normal file
|
After Width: | Height: | Size: 744 KiB |
BIN
assets/images/dungeons/corridor3_NS.png
Normal file
|
After Width: | Height: | Size: 739 KiB |
BIN
assets/images/dungeons/room_4x4_circle.png
Normal file
|
After Width: | Height: | Size: 905 KiB |
BIN
assets/images/dungeons/room_4x4_normal.png
Normal file
|
After Width: | Height: | Size: 907 KiB |
BIN
assets/images/dungeons/room_4x4_squeleton.png
Normal file
|
After Width: | Height: | Size: 968 KiB |
BIN
assets/images/dungeons/room_4x6_altar.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
assets/images/dungeons/room_4x6_tomb.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
@@ -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,30 @@ 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
|
||||
// 15 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' },
|
||||
{ type: 'room_4x4' }, { type: 'room_4x4' }, { type: 'room_4x4' }, { type: 'room_4x4' },
|
||||
{ type: 'room_4x4' }, { type: 'room_4x4' }, { type: 'room_4x4' },
|
||||
|
||||
// 8 rooms 4x6 (abstract)
|
||||
{ type: 'room_4x6' }, { type: 'room_4x6' }, { type: 'room_4x6' }, { type: 'room_4x6' },
|
||||
{ type: 'room_4x6' }, { type: 'room_4x6' }, { type: 'room_4x6' }, { type: 'room_4x6' },
|
||||
|
||||
// 10 corridors (abstract)
|
||||
{ type: 'corridor' }, { type: 'corridor' }, { type: 'corridor' }, { type: 'corridor' },
|
||||
{ type: 'corridor' }, { type: 'corridor' }, { type: 'corridor' }, { type: 'corridor' },
|
||||
{ type: 'corridor' }, { type: 'corridor' },
|
||||
|
||||
// 8 L-shapes (abstract)
|
||||
{ type: 'L' }, { type: 'L' }, { type: 'L' }, { type: 'L' },
|
||||
{ type: 'L' }, { type: 'L' }, { type: 'L' }, { type: 'L' },
|
||||
|
||||
// 6 T-junctions (abstract)
|
||||
{ type: 'T' }, { type: 'T' }, { type: 'T' }, { type: 'T' },
|
||||
{ type: 'T' }, { type: 'T' }
|
||||
];
|
||||
|
||||
// Fisher-Yates shuffle
|
||||
@@ -24,7 +46,13 @@ export class DungeonDeck {
|
||||
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");
|
||||
|
||||
// DEBUG: Fixed deck sequence for faster testing
|
||||
// Fixed deck removed.
|
||||
this.deck = this.deck.sort(() => Math.random() - 0.5);
|
||||
|
||||
console.log("%c[DungeonDeck] SHUFFLED! Top 5 cards:", "color: orange; font-weight: bold;",
|
||||
this.deck.slice(-5).map(c => c.type).join(', '));
|
||||
}
|
||||
|
||||
drawCard() {
|
||||
@@ -40,9 +68,87 @@ export class DungeonDeck {
|
||||
}
|
||||
|
||||
const card = this.deck.pop();
|
||||
console.log(`%c[DungeonDeck] Drew: ${card.type}`, "color: cyan");
|
||||
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 = 50) {
|
||||
const validTypes = this.getCompatibleTypes(originTileType);
|
||||
// console.log(`[DungeonDeck] Searching for ${validTypes.join('|')} (Origin: ${originTileType})`);
|
||||
|
||||
// Check cards currently in deck
|
||||
let checkedCount = 0;
|
||||
const initialDeckSize = this.deck.length;
|
||||
|
||||
// Loop until we find a match or give up
|
||||
// We use a pragmatic limit to prevent infinite loops (e.g., if even with discards we have 0 valid cards)
|
||||
const safetyLimit = this.deck.length + this.discardPile.length + 20;
|
||||
|
||||
for (let i = 0; i < safetyLimit; i++) {
|
||||
// If we have checked all cards relationships in the current deck, AND we have cards in discard...
|
||||
// Reshuffle discards in to give us a chance.
|
||||
if (checkedCount >= this.deck.length && this.discardPile.length > 0) {
|
||||
console.log(`[DungeonDeck] Cycled current deck (${this.deck.length} cards) without match. Reshuffling ${this.discardPile.length} discards.`);
|
||||
this.deck = [...this.deck, ...this.discardPile];
|
||||
this.discardPile = [];
|
||||
this.shuffleDeck(); // Shuffle everything together
|
||||
checkedCount = 0; // Reset counter since we have a new deck state
|
||||
}
|
||||
|
||||
if (this.deck.length === 0) {
|
||||
if (this.discardPile.length > 0) {
|
||||
// Current deck empty, pull from discards
|
||||
this.deck = [...this.discardPile];
|
||||
this.discardPile = [];
|
||||
this.shuffleDeck();
|
||||
checkedCount = 0;
|
||||
} else {
|
||||
console.error("No cards left anywhere!");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const card = this.deck.pop();
|
||||
checkedCount++;
|
||||
|
||||
// Check if this abstract type is compatible
|
||||
if (validTypes.includes(card.type)) {
|
||||
// Found one!
|
||||
console.log(`[DungeonDeck] MATCH! Accepted ${card.type}`);
|
||||
this.discardPile.push(card);
|
||||
return card;
|
||||
}
|
||||
|
||||
// Incompatible: Put back at bottom (unshift) to try next
|
||||
this.deck.unshift(card);
|
||||
}
|
||||
|
||||
console.warn(`Could not find compatible tile for ${originTileType} after ${safetyLimit} attempts. (Deck: ${this.deck.length}, Discard: ${this.discardPile.length})`);
|
||||
return null; // Return null (game will handle it, maybe stop exploring)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
476
src/dungeon/TileDefinitions.js
Normal file
@@ -0,0 +1,476 @@
|
||||
/**
|
||||
* 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'],
|
||||
doorCoordinates: {
|
||||
'N': { x: 0, y: 0 }, // Anchor: Top-Left of path (col 0,1 -> 0)
|
||||
'E': { x: 3, y: 2 } // Anchor: Top-Left of path (row 2,3 -> 2)
|
||||
}
|
||||
};
|
||||
|
||||
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'],
|
||||
doorCoordinates: {
|
||||
'S': { x: 0, y: 3 }, // Anchor: Top-Left (col 0,1 -> 0)
|
||||
'E': { x: 3, y: 0 } // Anchor: Top-Left (row 0,1 -> 0)
|
||||
}
|
||||
};
|
||||
|
||||
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'],
|
||||
doorCoordinates: {
|
||||
'W': { x: 0, y: 0 }, // Anchor: Top-Left (row 0,1 -> 0)
|
||||
'S': { x: 2, y: 3 } // Anchor: Top-Left (col 2,3 -> 2)
|
||||
}
|
||||
};
|
||||
|
||||
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'],
|
||||
doorCoordinates: {
|
||||
'W': { x: 0, y: 2 }, // Anchor: Top-Left (row 2,3 -> 2)
|
||||
'N': { x: 2, y: 0 } // Anchor: Top-Left (col 2,3 -> 2)
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 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'],
|
||||
doorCoordinates: {
|
||||
'N': { x: 0, y: 0 }, // Col 0,1 -> 0
|
||||
'E': { x: 3, y: 2 }, // Row 2,3 -> 2
|
||||
'S': { x: 0, y: 5 } // Col 0,1 -> 0
|
||||
}
|
||||
};
|
||||
|
||||
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'],
|
||||
doorCoordinates: {
|
||||
'W': { x: 0, y: 2 }, // Row 2,3 -> 2
|
||||
'N': { x: 2, y: 0 }, // Col 2,3 -> 2
|
||||
'E': { x: 5, y: 2 } // Row 2,3 -> 2
|
||||
}
|
||||
};
|
||||
|
||||
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'],
|
||||
doorCoordinates: {
|
||||
'W': { x: 0, y: 0 }, // Row 0,1 -> 0
|
||||
'S': { x: 2, y: 3 }, // Col 2,3 -> 2
|
||||
'E': { x: 5, y: 0 } // Row 0,1 -> 0
|
||||
}
|
||||
};
|
||||
|
||||
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'],
|
||||
doorCoordinates: {
|
||||
'W': { x: 0, y: 2 }, // Row 2,3 -> 2
|
||||
'N': { x: 2, y: 0 }, // Col 2,3 -> 2
|
||||
'S': { x: 2, y: 5 } // Col 2,3 -> 2
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// CORRIDORS (2x6 or 6x2 with 2-tile rows)
|
||||
// ============================================================================
|
||||
|
||||
const CORRIDOR_DOORS_EW = { 'E': { x: 5, y: 0 }, 'W': { x: 0, y: 0 } };
|
||||
const CORRIDOR_DOORS_NS = { 'N': { x: 0, y: 0 }, 'S': { x: 0, y: 5 } };
|
||||
|
||||
// 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'],
|
||||
doorCoordinates: CORRIDOR_DOORS_EW
|
||||
};
|
||||
|
||||
// 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'],
|
||||
doorCoordinates: CORRIDOR_DOORS_NS
|
||||
};
|
||||
|
||||
// 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'],
|
||||
doorCoordinates: CORRIDOR_DOORS_EW
|
||||
};
|
||||
|
||||
// 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'],
|
||||
doorCoordinates: CORRIDOR_DOORS_NS
|
||||
};
|
||||
|
||||
// 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'],
|
||||
doorCoordinates: CORRIDOR_DOORS_EW
|
||||
};
|
||||
|
||||
// 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'],
|
||||
doorCoordinates: CORRIDOR_DOORS_NS
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 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 [];
|
||||
}
|
||||
}
|
||||
606
src/main.js
@@ -1,15 +1,23 @@
|
||||
import './style.css';
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
import { io } from "socket.io-client";
|
||||
// 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
|
||||
const socketUrl = `http://${window.location.hostname}:3001`;
|
||||
const socket = io(socketUrl);
|
||||
// const socket = io(socketUrl);
|
||||
// MOCK SOCKET to avoid connection errors while debugging
|
||||
const socket = {
|
||||
on: (event, callback) => console.log(`[MockSocket] Listening for ${event}`),
|
||||
emit: (event, data) => console.log(`[MockSocket] Emitting ${event}`, data),
|
||||
id: 'mock-id'
|
||||
};
|
||||
let lobbyCode = null;
|
||||
|
||||
socket.on("connect", () => {
|
||||
@@ -78,9 +86,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 }
|
||||
@@ -97,11 +107,37 @@ function generateDungeon() {
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to create a flipped version of a tile
|
||||
function createFlippedTile(tileDef, direction) {
|
||||
const flipped = { ...tileDef };
|
||||
|
||||
// Create a new flipped walkability matrix
|
||||
const { walkability, width, height } = tileDef;
|
||||
const newWalkability = [];
|
||||
|
||||
if (direction === 'horizontal') {
|
||||
// Flip horizontally (mirror left-right)
|
||||
for (let y = 0; y < height; y++) {
|
||||
newWalkability[y] = [...walkability[y]].reverse();
|
||||
}
|
||||
flipped.id = tileDef.id + '_flipped_h';
|
||||
} else {
|
||||
// Flip vertically (mirror top-bottom)
|
||||
for (let y = 0; y < height; y++) {
|
||||
newWalkability[y] = walkability[height - 1 - y];
|
||||
}
|
||||
flipped.id = tileDef.id + '_flipped_v';
|
||||
}
|
||||
|
||||
flipped.walkability = newWalkability;
|
||||
// Note: We keep the same image, the flip is only for walkability logic
|
||||
// If you want visual flipping, you'd need to modify the rendering code
|
||||
|
||||
return flipped;
|
||||
}
|
||||
|
||||
// --- 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,41 +145,195 @@ 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 placementOffset = 0; // Offset for L-shapes based on selection
|
||||
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':
|
||||
// NEW RULE: Select L-shape based on corridor exit direction
|
||||
if (door.side === 'N') {
|
||||
// North exit (vertical corridor going up): choose LSE or LSW
|
||||
const lOptions = [
|
||||
{ tile: TileDefinitions.TILE_DEFINITIONS.L_SE, offset: -1 }, // LSE with offset -1
|
||||
{ tile: TileDefinitions.TILE_DEFINITIONS.L_WS, offset: 1 } // LSW with offset +1
|
||||
];
|
||||
const selected = lOptions[Math.floor(Math.random() * lOptions.length)];
|
||||
card = selected.tile;
|
||||
placementOffset = selected.offset;
|
||||
console.log(`✓ Selected ${card.id} for N exit with offset ${placementOffset}`);
|
||||
} else if (door.side === 'S') {
|
||||
// South exit (vertical corridor going down): choose LNE or LNW
|
||||
const lOptions = [
|
||||
{ tile: TileDefinitions.TILE_DEFINITIONS.L_NE, offset: 1 }, // LNE with offset 0
|
||||
{ tile: TileDefinitions.TILE_DEFINITIONS.L_WN, offset: -1 } // LNW with offset +2
|
||||
];
|
||||
const selected = lOptions[Math.floor(Math.random() * lOptions.length)];
|
||||
card = selected.tile;
|
||||
placementOffset = selected.offset;
|
||||
console.log(`✓ Selected ${card.id} for S exit with offset ${placementOffset}`);
|
||||
} else if (door.side === 'E') {
|
||||
// East exit: choose LWN or LWS
|
||||
const lOptions = [
|
||||
{ tile: TileDefinitions.TILE_DEFINITIONS.L_WN, offset: 1 }, // LWN with offset 0
|
||||
{ tile: TileDefinitions.TILE_DEFINITIONS.L_WS, offset: -1 } // LWS with offset +2
|
||||
];
|
||||
const selected = lOptions[Math.floor(Math.random() * lOptions.length)];
|
||||
card = selected.tile;
|
||||
placementOffset = selected.offset;
|
||||
console.log(`✓ Selected ${card.id} for E exit with offset ${placementOffset}`);
|
||||
} else if (door.side === 'W') {
|
||||
// West exit: choose LNE or LSE
|
||||
const lOptions = [
|
||||
{ tile: TileDefinitions.TILE_DEFINITIONS.L_NE, offset: 1 }, // LNE with offset +2
|
||||
{ tile: TileDefinitions.TILE_DEFINITIONS.L_SE, offset: -1 } // LSE with offset 0
|
||||
];
|
||||
const selected = lOptions[Math.floor(Math.random() * lOptions.length)];
|
||||
card = selected.tile;
|
||||
placementOffset = selected.offset;
|
||||
console.log(`✓ Selected ${card.id} for W exit with offset ${placementOffset}`);
|
||||
}
|
||||
break;
|
||||
case 'T':
|
||||
// T-junction placement based on corridor exit direction
|
||||
if (door.side === 'N') {
|
||||
// North exit: choose T_NES, T_WNS, or T_WSE
|
||||
const tOptions = [
|
||||
{ tile: TileDefinitions.TILE_DEFINITIONS.T_NES, offset: -1 }, // T_NES with offset -1
|
||||
{ tile: TileDefinitions.TILE_DEFINITIONS.T_WNS, offset: 1 }, // T_WNS with offset +1
|
||||
{ tile: TileDefinitions.TILE_DEFINITIONS.T_WSE, offset: 0 } // T_WSE with offset 0 (placeholder)
|
||||
];
|
||||
const selected = tOptions[Math.floor(Math.random() * tOptions.length)];
|
||||
card = selected.tile;
|
||||
placementOffset = selected.offset;
|
||||
console.log(`✓ Selected ${card.id} for N exit with offset ${placementOffset}`);
|
||||
} else {
|
||||
// For other exits, use all T-junctions (fallback)
|
||||
candidates = [...TileDefinitions.T_JUNCTIONS];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// If we already selected a specific card (L-shapes with N/S exits), skip validation
|
||||
if (card) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (candidates.length === 0) {
|
||||
console.warn(`No candidates found for type ${abstractCard.type}`);
|
||||
attempts++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// For rooms: randomly select one and optionally flip it
|
||||
if (abstractCard.type === 'room_4x4' || abstractCard.type === 'room_4x6') {
|
||||
const selectedRoom = candidates[Math.floor(Math.random() * candidates.length)];
|
||||
|
||||
// Randomly decide to flip horizontally or vertically
|
||||
const shouldFlip = Math.random() < 0.5;
|
||||
const flipDirection = Math.random() < 0.5 ? 'horizontal' : 'vertical';
|
||||
|
||||
if (shouldFlip) {
|
||||
card = createFlippedTile(selectedRoom, flipDirection);
|
||||
console.log(`✓ Selected ${selectedRoom.id} and flipped ${flipDirection}`);
|
||||
} else {
|
||||
card = selectedRoom;
|
||||
console.log(`✓ Selected ${selectedRoom.id} without flipping`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// For corridors and T-junctions: validate and select
|
||||
const fittingVariants = [];
|
||||
for (const variant of candidates) {
|
||||
const connectionResult = canConnectTiles(originRoom, variant, door.side);
|
||||
if (connectionResult.valid) {
|
||||
fittingVariants.push(variant);
|
||||
}
|
||||
}
|
||||
|
||||
if (fittingVariants.length > 0) {
|
||||
// RANDOM selection from fitting variants
|
||||
card = fittingVariants[Math.floor(Math.random() * fittingVariants.length)];
|
||||
console.log(`✓ Selected ${card.id} (${card.tileType}) randomly from ${fittingVariants.length} fitting variants`);
|
||||
} 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 relative to the new tile origin
|
||||
// We use external adjacent coordinates (-1 or width/height) to place the new tile
|
||||
// strictly adjacent to the previous one, preventing 1-cell overlaps.
|
||||
let entryGridX, entryGridY;
|
||||
|
||||
// Use explicit door coordinates if available (for asymmetric tiles like L/T)
|
||||
const doorCoords = nextTileDef.doorCoordinates ? nextTileDef.doorCoordinates[entrySide] : null;
|
||||
|
||||
if (entrySide === 'N') {
|
||||
entryGridX = Math.floor(nextTileDef.width / 2);
|
||||
entryGridY = 0;
|
||||
entryGridX = doorCoords ? doorCoords.x : Math.floor(nextTileDef.width / 2);
|
||||
entryGridY = -1;
|
||||
} else if (entrySide === 'S') {
|
||||
entryGridX = Math.floor(nextTileDef.width / 2);
|
||||
entryGridX = doorCoords ? doorCoords.x : Math.floor(nextTileDef.width / 2);
|
||||
entryGridY = nextTileDef.height;
|
||||
} else if (entrySide === 'E') {
|
||||
entryGridX = nextTileDef.width;
|
||||
entryGridY = Math.floor(nextTileDef.height / 2);
|
||||
entryGridY = doorCoords ? doorCoords.y : Math.floor(nextTileDef.height / 2);
|
||||
} else if (entrySide === 'W') {
|
||||
entryGridX = 0;
|
||||
entryGridY = Math.floor(nextTileDef.height / 2);
|
||||
entryGridX = -1;
|
||||
entryGridY = doorCoords ? doorCoords.y : 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
|
||||
// Manual placementOffset removed: Handled by explicit doorCoordinates via TileDefinitions.js
|
||||
|
||||
// Check for collisions
|
||||
if (!isAreaFree(newX, newY, nextTileDef.width, nextTileDef.height)) {
|
||||
console.warn("Cannot place room: Collision detected!");
|
||||
return null;
|
||||
@@ -151,17 +341,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,36 +364,48 @@ 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
|
||||
|
||||
// Check if this is an L or T tile to make exit open by default
|
||||
// For L and T tiles, exits are open passages, not closed doors
|
||||
const isOpenPassage = (nextTileDef.tileType === 'L' || nextTileDef.tileType === 'T');
|
||||
|
||||
const exitDoor = {
|
||||
side: exitDir,
|
||||
leadsTo: null, // Desconocido
|
||||
isOpen: false,
|
||||
leadsTo: null,
|
||||
isOpen: isOpenPassage, // Open by default for L/T
|
||||
isDoor: !isOpenPassage, // Not a blocking door for L/T
|
||||
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);
|
||||
// Calculate door coordinates using explicit definition or default centering
|
||||
if (nextTileDef.doorCoordinates && nextTileDef.doorCoordinates[exitDir]) {
|
||||
exitDoor.gridX = nextTileDef.doorCoordinates[exitDir].x;
|
||||
exitDoor.gridY = nextTileDef.doorCoordinates[exitDir].y;
|
||||
} else {
|
||||
// Default centering logic
|
||||
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 - 1;
|
||||
} else if (exitDir === 'E') {
|
||||
exitDoor.gridX = nextTileDef.width - 1;
|
||||
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);
|
||||
console.log(`✓ Tile ${newRoomId} (${nextTileDef.tileType}) created: ${nextTileDef.id} at (${newX}, ${newY})`);
|
||||
return newRoom;
|
||||
}
|
||||
|
||||
@@ -514,7 +721,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 +736,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 +752,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 +774,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 +802,171 @@ 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 } - whether the tiles can connect
|
||||
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);
|
||||
|
||||
// For now, we just check that both edges have at least some walkable cells
|
||||
// The actual alignment will be handled manually by the user
|
||||
const hasWalkableA = edgeA.some(cell => cell > 0);
|
||||
const hasWalkableB = edgeB.some(cell => cell > 0);
|
||||
|
||||
if (!hasWalkableA || !hasWalkableB) {
|
||||
console.warn('[ALIGN] One or both edges have no walkable cells');
|
||||
return { valid: false };
|
||||
}
|
||||
|
||||
console.log(`✓ [ALIGN] Both edges have walkable cells - connection allowed`);
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
// 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 } - whether the tiles can connect
|
||||
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 };
|
||||
}
|
||||
|
||||
// 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 };
|
||||
}
|
||||
|
||||
// Check walkability alignment
|
||||
const alignmentResult = validateWalkabilityAlignment(tileDefA, roomA.tile, tileDefB, null, exitSide);
|
||||
if (!alignmentResult.valid) {
|
||||
console.warn('Walkability alignment failed');
|
||||
return { valid: false };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
// --- CREACIÓN DE MARCADORES ---
|
||||
function createPathMarker(stepNumber) {
|
||||
const canvas = document.createElement('canvas');
|
||||
@@ -933,9 +1311,77 @@ async function animateMovement() {
|
||||
SESSION.selectedUnitId = null; // Deseleccionar unidad al terminar de mover
|
||||
updateSelectionVisuals();
|
||||
SESSION.isAnimating = false;
|
||||
|
||||
// Auto-exploration check for open passages (L/T tiles)
|
||||
checkAutoExploration(unit);
|
||||
|
||||
drawMinimap(); // Actualizar posición final del jugador
|
||||
}
|
||||
|
||||
// Check if unit is on an open passage leading to unexplored area
|
||||
function checkAutoExploration(unit) {
|
||||
// Find unit's current room
|
||||
const currentRoom = ROOMS.rooms.find(r => r.entities.some(e => e.id === unit.id));
|
||||
if (!currentRoom) return;
|
||||
|
||||
// DEBUG: Log unit position for troubleshooting
|
||||
console.log(`[AutoExplore] Unit at (${unit.x}, ${unit.y}) in Room ${currentRoom.id}`);
|
||||
|
||||
const door = currentRoom.doors.find(d => {
|
||||
const doorGlobalX = currentRoom.tile.x + d.gridX;
|
||||
const doorGlobalY = currentRoom.tile.y + d.gridY;
|
||||
|
||||
// DEBUG: Log each door check
|
||||
console.log(`[CheckDoor] ${d.id} (${d.side}): Global(${doorGlobalX}, ${doorGlobalY}) vs Unit(${unit.x}, ${unit.y})`);
|
||||
|
||||
// Check 1: Is Unit on the door line? (Internal or External adjacent)
|
||||
let isMainAxisMatch = false;
|
||||
let isCrossAxisMatch = false;
|
||||
|
||||
if (d.side === 'N' || d.side === 'S') {
|
||||
// Main axis = Y. Internal: doorGlobalY. External: +/- 1
|
||||
const deltaY = unit.y - doorGlobalY;
|
||||
// Allow 0 (internal) or 1 step out (external)
|
||||
if (d.side === 'N') isMainAxisMatch = (deltaY === 0 || deltaY === -1);
|
||||
else isMainAxisMatch = (deltaY === 0 || deltaY === 1);
|
||||
|
||||
// Cross axis = X. Allow +/- 1 for corridor width
|
||||
isCrossAxisMatch = Math.abs(unit.x - doorGlobalX) <= 1;
|
||||
|
||||
} else { // E or W
|
||||
// Main axis = X. Internal: doorGlobalX. External: +/- 1
|
||||
const deltaX = unit.x - doorGlobalX;
|
||||
// Allow 0 (internal) or 1 step out (external)
|
||||
if (d.side === 'W') isMainAxisMatch = (deltaX === 0 || deltaX === -1);
|
||||
else isMainAxisMatch = (deltaX === 0 || deltaX === 1);
|
||||
|
||||
// Cross axis = Y. Allow +/- 1 for corridor width
|
||||
isCrossAxisMatch = Math.abs(unit.y - doorGlobalY) <= 1;
|
||||
}
|
||||
|
||||
if (isMainAxisMatch && isCrossAxisMatch) {
|
||||
console.log(`[AutoExplore] MATCH! Door ${d.id} (${d.side}). Unit(${unit.x}, ${unit.y}) vs Door(${doorGlobalX}, ${doorGlobalY})`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (door) {
|
||||
console.log(`[AutoExplore] Found door at unit pos: ${door.id}, Open: ${door.isOpen}, LeadsTo: ${door.leadsTo}`);
|
||||
|
||||
if (door.isOpen && door.leadsTo === null) {
|
||||
console.log("[AutoExplore] Triggering exploration...");
|
||||
// Use timeout to allow movement animation to settle/feel natural
|
||||
setTimeout(() => {
|
||||
const newRoom = exploreRoom(currentRoom, door);
|
||||
if (newRoom) {
|
||||
renderRoom(newRoom);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -996,10 +1442,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 +1555,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;
|
||||
@@ -1139,6 +1593,14 @@ async function renderRoom(room) {
|
||||
tileMesh.position.z = originPos.z + (worldHeight / 2) - (CONFIG.CELL_SIZE / 2);
|
||||
tileMesh.position.y = 0;
|
||||
|
||||
// Random rotation for 4x4 rooms to add variety
|
||||
if (tileDef.id.includes('room_4x4')) {
|
||||
const rotations = [0, Math.PI / 2, Math.PI, Math.PI * 1.5];
|
||||
const randomRotation = rotations[Math.floor(Math.random() * rotations.length)];
|
||||
tileMesh.rotation.z = randomRotation;
|
||||
console.log(`[DEBUG] Room ${room.id} rotated ${(randomRotation * 180 / Math.PI).toFixed(0)}°`);
|
||||
}
|
||||
|
||||
console.log(`[DEBUG] renderRoom ${room.id} | MeshPos:`, tileMesh.position);
|
||||
|
||||
scene.add(tileMesh);
|
||||
@@ -1171,6 +1633,14 @@ async function renderRoom(room) {
|
||||
|
||||
for (const config of wallConfigs) {
|
||||
const wallSide = config.side;
|
||||
|
||||
// Skip wall rendering for L and T tiles on their exit sides
|
||||
if ((room.tileDef.tileType === 'L' || room.tileDef.tileType === 'T') &&
|
||||
room.tileDef.exits.includes(wallSide)) {
|
||||
console.log(`Skipping wall on ${wallSide} for ${room.tileDef.tileType} tile (exit side)`);
|
||||
continue; // Don't render wall on exit sides
|
||||
}
|
||||
|
||||
const door = doorsOnSides[wallSide];
|
||||
|
||||
// Función helper para crear un segmento de pared
|
||||
@@ -1374,7 +1844,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 +1877,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;
|
||||
|
||||