feat: Implement advanced tile mapping system with abstract deck

- Created TileDefinitions.js with centralized tile definitions
- Implemented abstract deck system (8 rooms 4x4, 4 rooms 4x6, 12 corridors, 10 L-shapes, 8 T-junctions)
- Added connection validation (type compatibility, exit direction, walkability alignment)
- Implemented corridor orientation filtering (EW/NS matching)
- Added exhaustive L/T variant selection with random choice
- Updated corridor definitions with EW and NS orientations
- Fixed ASSETS.tiles references throughout main.js
- Known issue: L/T offset alignment needs further debugging
This commit is contained in:
2025-12-29 02:09:34 +01:00
parent 83dc2b0234
commit c8cc35772f
23 changed files with 971 additions and 55 deletions

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 761 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1017 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1013 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1015 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1012 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 968 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

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

View File

@@ -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 [];
}
}

View File

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