Fix tile rendering dimensions and alignment, update tile definitions to use height
This commit is contained in:
279
DEVLOG.md
279
DEVLOG.md
@@ -1,55 +1,242 @@
|
||||
# Devlog - Sesión 1: Inicialización y Motor 3D
|
||||
# Devlog - Warhammer Quest (Versión Web 3D)
|
||||
|
||||
## Fecha: 30 de Diciembre, 2025
|
||||
## Sesión 4: Sistema de Colocación Manual - Estado Actual (2 Enero 2026)
|
||||
|
||||
### Objetivo
|
||||
Implementar un sistema de colocación manual de tiles donde el jugador:
|
||||
1. Mueve al bárbaro junto a una puerta
|
||||
2. Hace click en la puerta para abrir y revelar una nueva tile
|
||||
3. Puede rotar/mover la tile flotante antes de colocarla
|
||||
4. Confirma la colocación con el botón "Bajar"
|
||||
|
||||
### Trabajo Realizado
|
||||
|
||||
#### 1. Limpieza de Código
|
||||
- **Problema**: Código viejo de gameplay mezclado con nuevo sistema de construcción
|
||||
- **Solución**:
|
||||
- Copiado `GameEngine.js` y `main.js` a carpetas `old/` con fecha (20260102)
|
||||
- Reescrito versiones minimalistas enfocadas SOLO en construcción manual
|
||||
- Añadido sistema de jugador (bárbaro) con movimiento básico
|
||||
|
||||
#### 2. Flujo de Trabajo Implementado
|
||||
- ✅ Bárbaro aparece en primera tile
|
||||
- ✅ Click en bárbaro → selección (anillo amarillo)
|
||||
- ✅ Click en celda → movimiento
|
||||
- ✅ Click en puerta adyacente → abre y muestra tile flotante
|
||||
- ✅ Tile flotante a Y=3 con proyección verde/roja en suelo
|
||||
- ✅ Panel de controles (rotar, mover, bajar)
|
||||
|
||||
#### 3. Problemas Identificados y Parcialmente Resueltos
|
||||
|
||||
**Problema 1: Dimensiones de Tiles**
|
||||
- **Error**: Usaba `.length` en lugar de `.height` para dimensiones de variantes
|
||||
- **Impacto**: Rooms aparecían como 1x4 en lugar de 4x4
|
||||
- **Solución**: Cambiado a usar `variants.N.width` y `variants.N.height`
|
||||
|
||||
**Problema 2: Rotación de Texturas**
|
||||
- **Error**: Aplicaba rotación visual a planos con dimensiones ya rotadas
|
||||
- **Intento de solución**: Usar dimensiones BASE (NORTH) y aplicar rotación Z
|
||||
- **Estado**: Implementado pero no verificado completamente
|
||||
|
||||
**Problema 3: Posicionamiento Decimal**
|
||||
- **Error**: Coordenadas de placement eran decimales (1.5, 2.5)
|
||||
- **Solución**: Añadido `Math.round()` en `selectDoor` para anchor inicial
|
||||
|
||||
### Estado Actual - PROBLEMAS CRÍTICOS
|
||||
|
||||
**🔴 TILES NO SE COLOCAN CORRECTAMENTE**
|
||||
- Las dimensiones siguen siendo incorrectas para algunos tiles
|
||||
- La rotación visual no coincide con la proyección lógica
|
||||
- Las tiles se mueven de posición al bajarlas (T-junction)
|
||||
- Rooms aparecen deformadas o mal dimensionadas
|
||||
|
||||
### Causa Raíz del Problema
|
||||
|
||||
**Confusión entre dos conceptos:**
|
||||
1. **Dimensiones lógicas** (celdas que ocupa la tile) - calculadas desde `cells`
|
||||
2. **Dimensiones visuales** (tamaño del plano 3D) - deberían ser de la textura
|
||||
|
||||
**El problema fundamental:**
|
||||
- Las texturas están diseñadas para orientación NORTH
|
||||
- Cuando roto una tile, las CELDAS cambian de posición
|
||||
- Pero la TEXTURA sigue siendo la misma imagen
|
||||
- Estoy mezclando dimensiones de celdas rotadas con texturas sin rotar
|
||||
|
||||
### Archivos Modificados
|
||||
- `src/engine/game/GameEngine.js` - Reescrito (versión limpia)
|
||||
- `src/main.js` - Reescrito (versión limpia)
|
||||
- `src/view/GameRenderer.js` - Múltiples cambios en `addTile` y `showPlacementPreview`
|
||||
- `src/engine/dungeon/DungeonGenerator.js` - Añadido redondeo de coordenadas
|
||||
|
||||
### Próximos Pasos Recomendados
|
||||
1. **PAUSA**: Reiniciar ordenador, aumentar RAM
|
||||
2. **REVISIÓN COMPLETA**: Analizar la lógica de dimensiones vs rotación desde cero
|
||||
3. **SIMPLIFICACIÓN**: Quizás necesitamos un enfoque completamente diferente para el renderizado de tiles rotadas
|
||||
|
||||
---
|
||||
|
||||
## Sesión 3b: Corrección Crítica de Alineación (2 Enero 2026)
|
||||
|
||||
### Diagnóstico del "Desastre"
|
||||
Tras refactorizar el generador, las piezas aparecían desalineadas o superpuestas.
|
||||
- **Causa Raíz**: Inconsistencia de Sistemas de Coordenadas.
|
||||
- **Detalle**:
|
||||
- Los nuevos *Corridors* usaban definiciones de salida "Pre-Rotadas" (coordenadas relativas al ancla *después* de rotar).
|
||||
- Las *Habitaciones y Uniones* (Rooms/Junctions) usaban definiciones "Locales" (coordenadas relativas al tile sin rotar).
|
||||
- El nuevo `DungeonGenerator` asumía que TODAS las definiciones eran "Pre-Rotadas" y sumaba las coordenadas directamente.
|
||||
- Esto causaba que Rooms y Junctions rotados hacia el Este/Sur/Oeste se calcularan en posiciones totalmente erróneas (fuera del tile).
|
||||
|
||||
### Solución Implementada
|
||||
Se unificó todo el sistema al paradigma **Pre-Rotated Relative Offsets**:
|
||||
1. **Actualización Masiva**: Se reescribieron las definiciones `exitsByRotation` para `corridor_corner`, `junction_t`, `room_dungeon` y `room_objective`.
|
||||
2. **Lógica Geométrica**:
|
||||
- **NORTH**: X positivo, Y positivo.
|
||||
- **EAST**: X positivo, Y negativo (el tile crece hacia abajo).
|
||||
- **SOUTH**: X negativo, Y negativo (el tile crece hacia izquierda/abajo).
|
||||
- **WEST**: X negativo, Y positivo (el tile crece hacia izquierda/arriba).
|
||||
3. **Verificación**: Ahora todas las definiciones de salida coinciden con la geometría real generada por `GridSystem` en cada rotación.
|
||||
|
||||
### Estado
|
||||
- ✅ **Alineación**: Debería ser perfecta para todos los tipos de tiles y rotaciones.
|
||||
- ✅ **Código**: Mucho más limpio y sin cálculos trigonométricos en tiempo de ejecución.
|
||||
|
||||
---
|
||||
|
||||
## Sesión 3: Refinamiento de Generación de Mazmorras (2 Enero 2026)
|
||||
|
||||
### Resumen General
|
||||
En esta sesión se ha establecido la base completa del motor de juego para **Warhammer Quest (Versión Web 3D)**. Se ha pasado de un concepto inicial a una aplicación dockerizada con generación procedimental de mazmorras y visualización isométrica en 3D.
|
||||
Sesión enfocada en resolver problemas fundamentales de alineación de tiles en la generación de mazmorras. Se identificó que el problema raíz estaba en el uso de cálculos dinámicos de rotación para tiles con definiciones estáticas de salidas.
|
||||
|
||||
### Trabajo Realizado
|
||||
|
||||
#### 1. Análisis del Problema de Rotación
|
||||
- **Descubrimiento clave**: Las definiciones de tiles en `TileDefinitions.js` tienen salidas con coordenadas y direcciones **estáticas** (relativas a orientación Norte).
|
||||
- **Problema**: Los cálculos de `getRotatedOffset()` y `getRotatedDirection()` asumían tiles cuadrados y simétricos, fallando con tiles asimétricos (corridors 2x6, L-corners con forma irregular).
|
||||
- **Impacto**: Corridors y L-corners se colocaban desalineados, creando gaps entre tiles.
|
||||
|
||||
#### 2. Implementación de Configuraciones de Corridor
|
||||
- **Contexto del manual**: Los corridors tienen 1 puerta fija y 1 puerta variable con 3 posiciones posibles (Norte, Este lateral, Oeste lateral).
|
||||
- **Solución inicial**: Creamos `exitConfigurations` para corridors con 3 opciones de salida.
|
||||
- **Decisión**: Corridors **NO rotan**, solo cambian de configuración de salida.
|
||||
- **Resultado**: Simplifica la lógica pero no resuelve el problema de otros tiles.
|
||||
|
||||
#### 3. Enfoque de Salidas Explícitas por Rotación
|
||||
- **Decisión de diseño**: Cambiar de cálculos dinámicos a definiciones explícitas.
|
||||
- **Implementación**: Añadir `exitsByRotation` a cada tile que rota (L-Corner, T-Junction, Rooms).
|
||||
- **Estructura**:
|
||||
```javascript
|
||||
exitsByRotation: {
|
||||
[DIRECTIONS.NORTH]: [ /* exits for North orientation */ ],
|
||||
[DIRECTIONS.EAST]: [ /* exits for East orientation */ ],
|
||||
[DIRECTIONS.SOUTH]: [ /* exits for South orientation */ ],
|
||||
[DIRECTIONS.WEST]: [ /* exits for West orientation */ ]
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Tiles Actualizados
|
||||
- ✅ **L-Corner (corridor_corner)**: 4 rotaciones definidas explícitamente
|
||||
- ✅ **T-Junction (junction_t)**: 4 rotaciones definidas explícitamente
|
||||
- ✅ **Dungeon Room (room_dungeon)**: 4 rotaciones definidas explícitamente
|
||||
- ✅ **Objective Room (room_objective)**: 4 rotaciones definidas explícitamente
|
||||
- ✅ **Corridors**: Mantienen `exitConfigurations` (3 opciones, sin rotación)
|
||||
|
||||
### Estado Actual
|
||||
|
||||
#### Completado
|
||||
- ✅ Definiciones de `exitsByRotation` para todos los tiles que rotan
|
||||
- ✅ Definiciones de `exitConfigurations` para corridors
|
||||
- ✅ Logs de diagnóstico mejorados para debugging
|
||||
|
||||
#### En Progreso
|
||||
- ⚠️ **DungeonGenerator.js**: Lógica de `step()` parcialmente actualizada
|
||||
- Se añadió detección de `exitsByRotation`
|
||||
- **PROBLEMA**: Errores de sintaxis en bucles anidados (11 errores de linting)
|
||||
- Necesita reestructuración completa de la lógica de iteración
|
||||
|
||||
#### Pendiente
|
||||
- ❌ Eliminar cálculos de rotación obsoletos (`getRotatedOffset`, `getRotatedDirection`) una vez que `exitsByRotation` funcione
|
||||
- ❌ Pruebas completas de alineación con las nuevas definiciones
|
||||
- ❌ Limpieza de código y eliminación de logs de debug
|
||||
|
||||
### Problemas Identificados
|
||||
|
||||
1. **Bucles anidados incorrectos**: La lógica actual tiene 3 niveles de bucles mal estructurados:
|
||||
- Configuraciones (para corridors)
|
||||
- Rotaciones (para otros tiles)
|
||||
- Salidas (para cada rotación/configuración)
|
||||
|
||||
2. **Mezcla de enfoques**: El código intenta manejar simultáneamente:
|
||||
- `exitConfigurations` (corridors sin rotación)
|
||||
- `exitsByRotation` (otros tiles con rotación)
|
||||
- Cálculos dinámicos (legacy, debe eliminarse)
|
||||
|
||||
### Próximos Pasos (Mañana)
|
||||
|
||||
#### Prioridad Alta
|
||||
1. **Reestructurar `step()` en DungeonGenerator.js**:
|
||||
- Separar lógica para corridors (exitConfigurations) vs otros tiles (exitsByRotation)
|
||||
- Simplificar bucles anidados
|
||||
- Eliminar cálculos de rotación cuando se use `exitsByRotation`
|
||||
|
||||
2. **Verificar alineación**:
|
||||
- Probar cada tipo de tile en todas sus rotaciones
|
||||
- Confirmar que no hay gaps ni overlaps
|
||||
|
||||
#### Prioridad Media
|
||||
3. **Limpieza de código**:
|
||||
- Eliminar `getRotatedOffset()` y `getRotatedDirection()` (obsoletos)
|
||||
- Remover logs de debug innecesarios
|
||||
- Documentar la nueva estructura
|
||||
|
||||
4. **Optimización**:
|
||||
- Revisar performance de la generación
|
||||
- Considerar caché de configuraciones válidas
|
||||
|
||||
### Notas Técnicas
|
||||
|
||||
**Ventajas del enfoque `exitsByRotation`**:
|
||||
- ✅ Elimina errores de cálculo matemático
|
||||
- ✅ Funciona para cualquier forma de tile (simétrico o asimétrico)
|
||||
- ✅ Fácil de verificar manualmente
|
||||
- ✅ Sin ambigüedad en las conexiones
|
||||
|
||||
**Desventajas**:
|
||||
- ❌ Más código (cada tile requiere 4 definiciones)
|
||||
- ❌ Propenso a errores manuales al escribir coordenadas
|
||||
- ❌ Más difícil de mantener si cambian dimensiones de tiles
|
||||
|
||||
### Decisiones de Diseño
|
||||
|
||||
1. **Corridors no rotan**: Solo cambian configuración de puerta (3 opciones)
|
||||
2. **Otros tiles rotan**: Usan `exitsByRotation` con 4 orientaciones explícitas
|
||||
3. **Sin cálculos dinámicos**: Las salidas ya están en coordenadas correctas para cada rotación
|
||||
4. **Logs detallados**: Mantener durante debugging, eliminar en producción
|
||||
|
||||
---
|
||||
|
||||
## Sesión 2: Refinamiento de Generación (31 Diciembre 2025)
|
||||
|
||||
### Resumen
|
||||
Implementación de sistema de exploración guiada por jugador y mejoras en la interfaz de usuario.
|
||||
|
||||
### Hitos Alcanzados
|
||||
- ✅ Sistema de movimiento de jugador implementado
|
||||
- ✅ Selección de puertas para exploración
|
||||
- ✅ Modal de confirmación para abrir puertas
|
||||
- ✅ Generación paso a paso según decisiones del jugador
|
||||
- ✅ Cámara con transiciones suaves entre vistas
|
||||
- ✅ Mejoras en UI (botones, controles, feedback visual)
|
||||
|
||||
#### 1. Infraestructura
|
||||
* **Dockerización**: Se creó un entorno conteinerizado usando `Dockerfile` y `docker-compose`. La aplicación corre sobre **Nginx** (Frontend) y se construye con **Node.js/Vite**.
|
||||
* **Estructura del Proyecto**: Configuración de `package.json`, `index.html` limpio, y carpetas organizadas (`src/engine`, `src/view`, `public/assets`).
|
||||
---
|
||||
|
||||
#### 2. Motor de Juego (Engine)
|
||||
* **GridSystem**: Implementación de un sistema de coordenadas global y local. Soporte para rotación de baldosas y detección de colisiones mediante matrices de ocupación (`layout`).
|
||||
* **DungeonGenerator**: Lógica central de generación.
|
||||
* Gestiona el bucle de "Paso a paso" (Step).
|
||||
* Conecta baldosas basándose en las salidas (`Exits`) disponibles.
|
||||
* Valida superposiciones antes de colocar una pieza.
|
||||
* **DungeonDeck (Reglas)**: Implementación fiel al libro de reglas.
|
||||
* Mazo de 13 cartas.
|
||||
* Mezcla inicial de cartas de mazmorra y pasillo.
|
||||
* Inserción de la "Habitación Objetivo" en la segunda mitad (últimas 7 cartas) para asegurar una duración de partida adecuada.
|
||||
* **TileDefinitions**: Base de datos de baldosas (Corridor, Corner, T-Junction, Rooms).
|
||||
* Definición de dimensiones físicas y lógicas.
|
||||
* Definición de puntos de salida (Norte, Sur, Este, Oeste).
|
||||
* Asignación de texturas.
|
||||
## Sesión 1: Inicialización y Motor 3D (30 Diciembre 2025)
|
||||
|
||||
#### 3. Visualización 3D (Three.js)
|
||||
* **GameRenderer**:
|
||||
* Escena básica con iluminación ambiental y direccional.
|
||||
* **Visualización de Debug**: `GridHelper` (suelo) y `AxesHelper` (ejes).
|
||||
* **Renderizado de Baldosas**:
|
||||
* Creación de "Grill" (rejilla de alambre) para visualizar celdas individuales lógica.
|
||||
* Implementación de `TextureLoader` para cargar imágenes PNG sobre planos 3D.
|
||||
* **CameraManager**:
|
||||
* Cámara Isométrica (`OrthographicCamera`).
|
||||
* Controles de órbita fijos (N, S, E, O).
|
||||
* Zoom y Panoramización.
|
||||
* **Assets**: Integración de texturas (`.png`) para baldosas, movidas a la carpeta `public/assets` para su correcta carga en el navegador.
|
||||
### Resumen
|
||||
Establecimiento de la base completa del motor de juego con generación procedimental y visualización 3D.
|
||||
|
||||
### Estado Actual
|
||||
### Estado Actual
|
||||
### Estado Actual
|
||||
* El generador crea mazmorras y las visualiza en 3D con texturas.
|
||||
* **Problemas de Alineación**: Persisten desajustes en las conexiones de mazmorra (efecto zig-zag en puertas dobles) en la generación automática.
|
||||
* **Decisión de Diseño**: Se detiene el refinamiento de la generación automática aleatoria. El enfoque cambia a implementar la **Exploración Guiada por el Jugador**, donde la mazmorra se genera pieza a pieza según la decisión del usuario, lo que simplificará la lógica de conexión y evitará casos límite generados por el azar puro.
|
||||
|
||||
### Próximos Pasos (Siguiente Sesión)
|
||||
* Implementar al Jugador (Héroe) y su movimiento.
|
||||
* Desactivar la generación automática (`generator.step()` automático).
|
||||
* Crear UI para que el jugador elija "Explorar" en una salida específica.
|
||||
* Generar solo la siguiente pieza conectada a la salida elegida.
|
||||
* Implementar la interfaz de usuario (UI) para mostrar cartas y estado del juego.
|
||||
* Añadir modelos 3D para héroes y monstruos.
|
||||
### Hitos Alcanzados
|
||||
- ✅ Infraestructura dockerizada (Nginx + Node.js/Vite)
|
||||
- ✅ Motor de juego (GridSystem, DungeonGenerator, DungeonDeck)
|
||||
- ✅ Visualización 3D con Three.js
|
||||
- ✅ Sistema de cámara isométrica
|
||||
- ✅ Carga de texturas y assets
|
||||
|
||||
153
implementación/coordinate_system_analysis.md
Normal file
153
implementación/coordinate_system_analysis.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# Análisis del Sistema de Coordenadas
|
||||
|
||||
## GridSystem - Cómo Funciona la Rotación
|
||||
|
||||
En `GridSystem.getGlobalCells()`, las transformaciones son:
|
||||
|
||||
```javascript
|
||||
// Local coords: lx (columna), ly (fila convertida desde matriz)
|
||||
// ly = (numberOfRows - 1) - row // Fila 0 de matriz = Y más alto
|
||||
|
||||
switch (rotation) {
|
||||
case NORTH:
|
||||
gx = startX + lx;
|
||||
gy = startY + ly;
|
||||
break;
|
||||
case SOUTH:
|
||||
gx = startX - lx;
|
||||
gy = startY - ly;
|
||||
break;
|
||||
case EAST:
|
||||
gx = startX + ly;
|
||||
gy = startY - lx;
|
||||
break;
|
||||
case WEST:
|
||||
gx = startX - ly;
|
||||
gy = startY + lx;
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
## Ejemplo: Corridor 2x6 (Ancho=2, Largo=6)
|
||||
|
||||
Layout en matriz (fila 0 arriba, fila 5 abajo):
|
||||
```
|
||||
[1, 1] // Fila 0 -> ly=5 (norte)
|
||||
[1, 1] // Fila 1 -> ly=4
|
||||
[1, 1] // Fila 2 -> ly=3
|
||||
[1, 1] // Fila 3 -> ly=2
|
||||
[1, 1] // Fila 4 -> ly=1
|
||||
[1, 1] // Fila 5 -> ly=0 (sur)
|
||||
```
|
||||
|
||||
### Rotación NORTH (Sin rotar)
|
||||
- Anchor en (0,0)
|
||||
- Celdas ocupadas: (0,0) (1,0) (0,1) (1,1) ... (0,5) (1,5)
|
||||
- Salida SUR: celdas (0,0) y (1,0) -> exits en estas posiciones mirando SOUTH
|
||||
- Salida NORTE: celdas (0,5) y (1,5) -> exits en estas posiciones mirando NORTH
|
||||
|
||||
**Definición de exits para NORTH (Config Straight):**
|
||||
```javascript
|
||||
{ x: 0, y: 0, direction: SOUTH },
|
||||
{ x: 1, y: 0, direction: SOUTH },
|
||||
{ x: 0, y: 5, direction: NORTH },
|
||||
{ x: 1, y: 5, direction: NORTH }
|
||||
```
|
||||
|
||||
### Rotación EAST
|
||||
- Anchor en (0,0)
|
||||
- Transformación: gx = 0 + ly, gy = 0 - lx
|
||||
- Celda local (lx=0, ly=0): Global (0, 0)
|
||||
- Celda local (lx=1, ly=0): Global (0, -1)
|
||||
- Celda local (lx=0, ly=5): Global (5, 0)
|
||||
- Celda local (lx=1, ly=5): Global (5, -1)
|
||||
|
||||
El corridor ahora es horizontal, creciendo hacia la derecha (+X), con ancho en dirección -Y.
|
||||
|
||||
**Celdas ocupadas:**
|
||||
- (0,0), (0,-1) - extremo oeste
|
||||
- (1,0), (1,-1)
|
||||
- (2,0), (2,-1)
|
||||
- (3,0), (3,-1)
|
||||
- (4,0), (4,-1)
|
||||
- (5,0), (5,-1) - extremo este
|
||||
|
||||
**Definición de exits para EAST (Config Straight):**
|
||||
Salida WEST (era SUR): en el extremo oeste
|
||||
```javascript
|
||||
{ x: 0, y: 0, direction: WEST },
|
||||
{ x: 0, y: -1, direction: WEST }
|
||||
```
|
||||
|
||||
Salida EAST (era NORTE): en el extremo este
|
||||
```javascript
|
||||
{ x: 5, y: 0, direction: EAST },
|
||||
{ x: 5, y: -1, direction: EAST }
|
||||
```
|
||||
|
||||
### Rotación SOUTH
|
||||
- Anchor en (0,0)
|
||||
- Transformación: gx = 0 - lx, gy = 0 - ly
|
||||
- Celda local (lx=0, ly=0): Global (0, 0)
|
||||
- Celda local (lx=1, ly=0): Global (-1, 0)
|
||||
- Celda local (lx=0, ly=5): Global (0, -5)
|
||||
- Celda local (lx=1, ly=5): Global (-1, -5)
|
||||
|
||||
El corridor crece hacia abajo (-Y) y hacia la izquierda (-X).
|
||||
|
||||
**Celdas ocupadas:**
|
||||
- (0,0), (-1,0) - extremo norte (era sur)
|
||||
- (0,-1), (-1,-1)
|
||||
- (0,-2), (-1,-2)
|
||||
- (0,-3), (-1,-3)
|
||||
- (0,-4), (-1,-4)
|
||||
- (0,-5), (-1,-5) - extremo sur (era norte)
|
||||
|
||||
**Definición de exits para SOUTH (Config Straight):**
|
||||
Salida NORTH (era SUR original): en el extremo norte
|
||||
```javascript
|
||||
{ x: 0, y: 0, direction: NORTH },
|
||||
{ x: -1, y: 0, direction: NORTH }
|
||||
```
|
||||
|
||||
Salida SOUTH (era NORTE original): en el extremo sur
|
||||
```javascript
|
||||
{ x: 0, y: -5, direction: SOUTH },
|
||||
{ x: -1, y: -5, direction: SOUTH }
|
||||
```
|
||||
|
||||
### Rotación WEST
|
||||
- Anchor en (0,0)
|
||||
- Transformación: gx = 0 - ly, gy = 0 + lx
|
||||
- Celda local (lx=0, ly=0): Global (0, 0)
|
||||
- Celda local (lx=1, ly=0): Global (0, 1)
|
||||
- Celda local (lx=0, ly=5): Global (-5, 0)
|
||||
- Celda local (lx=1, ly=5): Global (-5, 1)
|
||||
|
||||
El corridor es horizontal, creciendo hacia la izquierda (-X), con ancho en dirección +Y.
|
||||
|
||||
**Celdas ocupadas:**
|
||||
- (0,0), (0,1) - extremo este (era sur)
|
||||
- (-1,0), (-1,1)
|
||||
- (-2,0), (-2,1)
|
||||
- (-3,0), (-3,1)
|
||||
- (-4,0), (-4,1)
|
||||
- (-5,0), (-5,1) - extremo oeste (era norte)
|
||||
|
||||
**Definición de exits para WEST (Config Straight):**
|
||||
Salida EAST (era SUR original): en el extremo este
|
||||
```javascript
|
||||
{ x: 0, y: 0, direction: EAST },
|
||||
{ x: 0, y: 1, direction: EAST }
|
||||
```
|
||||
|
||||
Salida WEST (era NORTE original): en el extremo oeste
|
||||
```javascript
|
||||
{ x: -5, y: 0, direction: WEST },
|
||||
{ x: -5, y: 1, direction: WEST }
|
||||
```
|
||||
|
||||
## Conclusión
|
||||
Las definiciones actuales de corridors están ✅ **CORRECTAS**.
|
||||
|
||||
El problema debe estar en el `DungeonGenerator.js`.
|
||||
34
implementación/implementation_plan_rotation.md
Normal file
34
implementación/implementation_plan_rotation.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Paradigm Shift: Explicit Rotations & Corridors
|
||||
|
||||
## Goal
|
||||
To eliminate dynamic rotation calculations which caused alignment issues, specifically for asymmetric tiles like Corridors (2x6). We will implement explicit exit definitions for all 4 rotations for Corridors, just like we did for Rooms.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Update `TileDefinitions.js`
|
||||
- Modify `corridor_straight` and `corridor_steps`.
|
||||
- Change `exitConfigurations` from an array of simple arrays to an array of objects.
|
||||
- Each object will represent a configuration (Straight, Corner-Left, Corner-Right) but will contain `exitsByRotation` for `NORTH`, `EAST`, `SOUTH`, `WEST`.
|
||||
- Pre-calculate coordinates for these rotations based on the 2x6 layout.
|
||||
|
||||
### 2. Rewrite `DungeonGenerator.js`
|
||||
- **Simplify `step()`**:
|
||||
- Iterate through `TileDefinitions` that might apply.
|
||||
- Handle `pendingExits`.
|
||||
- **New Alignment Logic**:
|
||||
- Pick a card.
|
||||
- If it has `exitConfigurations` (Corridors), iterate them.
|
||||
- Inside, iterate 4 Rotations.
|
||||
- Inside, iterate Exits.
|
||||
- If it has simple definitions (Rooms), iterate 4 Rotations.
|
||||
- Inside, iterate Exits.
|
||||
- **Placement**:
|
||||
- Calculate `GlobalPos = TargetConnection - LocalExitPos`.
|
||||
- Check Validity (Opposite Direction).
|
||||
- Check `canPlace`.
|
||||
- **Cleanup**: Remove deprecated methods like `getRotatedOffset`.
|
||||
|
||||
## Expected Outcome
|
||||
- Corridors align perfectly in all directions.
|
||||
- No gaps.
|
||||
- Code is easier to understand (no complex matrix rotation math in JS, just lookups).
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 242 KiB After Width: | Height: | Size: 245 KiB |
@@ -1,85 +1,68 @@
|
||||
|
||||
import { TILES } from './TileDefinitions.js';
|
||||
|
||||
export class DungeonDeck {
|
||||
|
||||
|
||||
constructor() {
|
||||
this.cards = [];
|
||||
this.discards = [];
|
||||
// We don't initialize automatically anymore
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the deck according to the specific Warhammer Quest rules.
|
||||
* Rulebook steps:
|
||||
* 1. Take 6 random Dungeon Cards (Bottom pool).
|
||||
* 2. Add Objective Room card to Bottom pool.
|
||||
* 3. Shuffle Bottom pool (7 cards).
|
||||
* 4. Take 6 random Dungeon Cards (Top pool).
|
||||
* 5. Stack Top pool on Bottom pool.
|
||||
* Total: 13 cards.
|
||||
*
|
||||
* @param {string} objectiveTileId - ID of the objective/exit room.
|
||||
*/
|
||||
generateMissionDeck(objectiveTileId) {
|
||||
console.log("🔍 Inspecting TILES object keys:", Object.keys(TILES));
|
||||
this.cards = [];
|
||||
|
||||
// 1. Create a "Pool" of standard dungeon tiles (Rooms & Corridors)
|
||||
// We replicate the physical deck distribution first
|
||||
// 1. Create a "Pool" of standard dungeon tiles
|
||||
let pool = [];
|
||||
const composition = [
|
||||
{ id: 'room_dungeon', count: 6 },
|
||||
// Objective room is special, handled separately
|
||||
{ id: 'corridor_straight', count: 7 },
|
||||
{ id: 'corridor_steps', count: 1 },
|
||||
{ id: 'corridor_corner', count: 1 },
|
||||
{ id: 'corridor_corner', count: 1 }, // L-Shape
|
||||
{ id: 'junction_t', count: 3 }
|
||||
];
|
||||
|
||||
composition.forEach(item => {
|
||||
const tileDef = TILES.find(t => t.id === item.id);
|
||||
// FIXED: Access by Key string directly
|
||||
const tileDef = TILES[item.id];
|
||||
|
||||
if (tileDef) {
|
||||
for (let i = 0; i < item.count; i++) {
|
||||
pool.push(tileDef);
|
||||
}
|
||||
} else {
|
||||
console.error(`❌ Missing Tile Definition for ID: ${item.id}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Helper to pull random cards
|
||||
const drawRandom = (source, count) => {
|
||||
const drawn = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (source.length === 0) break;
|
||||
const idx = Math.floor(Math.random() * source.length);
|
||||
drawn.push(source[idx]);
|
||||
source.splice(idx, 1); // Remove from pool
|
||||
source.splice(idx, 1);
|
||||
}
|
||||
return drawn;
|
||||
};
|
||||
|
||||
// --- Step 1 & 2: Bottom Pool (6 Random + Objective) ---
|
||||
// --- Step 1 & 2: Bottom Pool ---
|
||||
const bottomPool = drawRandom(pool, 6);
|
||||
|
||||
// Add Objective Card
|
||||
const objectiveDef = TILES.find(t => t.id === objectiveTileId);
|
||||
const objectiveDef = TILES[objectiveTileId];
|
||||
if (objectiveDef) {
|
||||
bottomPool.push(objectiveDef);
|
||||
} else {
|
||||
console.error("Objective Tile ID not found:", objectiveTileId);
|
||||
// Fallback: Add a generic room if objective missing?
|
||||
}
|
||||
|
||||
// --- Step 3: Shuffle Bottom Pool ---
|
||||
this.shuffleArray(bottomPool);
|
||||
|
||||
// --- Step 4: Top Pool (6 Random) ---
|
||||
// --- Step 4: Top Pool ---
|
||||
const topPool = drawRandom(pool, 6);
|
||||
// Note: No shuffle explicitly needed for Top Pool if drawn randomly,
|
||||
// but shuffling ensures random order of the 6 drawn.
|
||||
this.shuffleArray(topPool);
|
||||
|
||||
// --- Step 5: Stack (Top on Bottom) ---
|
||||
// Array[0] is the "Top" card (first to be drawn)
|
||||
// --- Step 5: Stack ---
|
||||
this.cards = [...topPool, ...bottomPool];
|
||||
|
||||
console.log(`Deck Generated: ${this.cards.length} cards.`);
|
||||
@@ -93,15 +76,12 @@ export class DungeonDeck {
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (this.cards.length === 0) {
|
||||
return null; // Deck empty
|
||||
}
|
||||
return this.cards.shift(); // Take from top
|
||||
if (this.cards.length === 0) return null;
|
||||
return this.cards.shift();
|
||||
}
|
||||
|
||||
// Useful for Campaign logic: Insert a specific card at position
|
||||
insertCard(tileId, position = 0) {
|
||||
const tileDef = TILES.find(t => t.id === tileId);
|
||||
const tileDef = TILES[tileId];
|
||||
if (tileDef) {
|
||||
this.cards.splice(position, 0, tileDef);
|
||||
}
|
||||
|
||||
@@ -1,296 +1,317 @@
|
||||
|
||||
import { DIRECTIONS } from './Constants.js';
|
||||
import { GridSystem } from './GridSystem.js';
|
||||
import { DungeonDeck } from './DungeonDeck.js';
|
||||
import { TILES } from './TileDefinitions.js';
|
||||
|
||||
const PLACEMENT_STATE = {
|
||||
WAITING_DOOR: 'WAITING_DOOR',
|
||||
PLACING_TILE: 'PLACING_TILE',
|
||||
COMPLETE: 'COMPLETE'
|
||||
};
|
||||
|
||||
export class DungeonGenerator {
|
||||
constructor() {
|
||||
this.grid = new GridSystem();
|
||||
this.deck = new DungeonDeck();
|
||||
this.pendingExits = []; // Array of global {x, y, direction}
|
||||
this.placedTiles = [];
|
||||
this.isComplete = false;
|
||||
this.availableExits = []; // Exits where player can choose to expand
|
||||
|
||||
// Placement State
|
||||
this.state = PLACEMENT_STATE.WAITING_DOOR;
|
||||
this.currentCard = null;
|
||||
this.placementRotation = DIRECTIONS.NORTH;
|
||||
this.placementX = 0;
|
||||
this.placementY = 0;
|
||||
this.selectedExit = null;
|
||||
|
||||
// Callbacks for UI
|
||||
this.onStateChange = null;
|
||||
this.onPlacementUpdate = null;
|
||||
}
|
||||
|
||||
startDungeon(missionConfig) {
|
||||
// 1. Prepare Deck (Rulebook: 13 cards, 6+1+6)
|
||||
// We need an objective tile ID from the config
|
||||
const objectiveId = missionConfig.type === 'quest' ? 'room_objective' : 'room_dungeon'; // Fallback for now
|
||||
const objectiveId = missionConfig?.type === 'quest' ? 'room_objective' : 'room_dungeon';
|
||||
this.deck.generateMissionDeck(objectiveId);
|
||||
|
||||
// 2. Rulebook Step 4: "Flip the first card. This is the entrance."
|
||||
const startCard = this.deck.draw();
|
||||
|
||||
if (!startCard) {
|
||||
console.error("Deck is empty on start!");
|
||||
// 1. Draw and place first card automatically at origin
|
||||
const firstCard = this.deck.draw();
|
||||
if (!firstCard) {
|
||||
console.error("❌ Empty deck");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Place the Entry Tile at (0,0)
|
||||
// We assume rotation NORTH by default for the first piece
|
||||
const startInstance = {
|
||||
id: `tile_0_${startCard.id}`,
|
||||
defId: startCard.id,
|
||||
x: 0,
|
||||
y: 0,
|
||||
rotation: DIRECTIONS.NORTH
|
||||
};
|
||||
this.placeCardFinal(firstCard, 0, 0, DIRECTIONS.NORTH);
|
||||
console.log(`🏰 Dungeon started with ${firstCard.name}`);
|
||||
|
||||
if (this.grid.canPlace(startCard, 0, 0, DIRECTIONS.NORTH)) {
|
||||
this.grid.placeTile(startInstance, startCard);
|
||||
this.placedTiles.push(startInstance);
|
||||
this.addExitsToQueue(startInstance, startCard);
|
||||
console.log(`Dungeon started with ${startCard.name}`);
|
||||
} else {
|
||||
console.error("Failed to place starting tile (Grid collision at 0,0?)");
|
||||
}
|
||||
// 2. Transition to door selection
|
||||
this.state = PLACEMENT_STATE.WAITING_DOOR;
|
||||
this.notifyStateChange();
|
||||
}
|
||||
|
||||
step() {
|
||||
if (this.isComplete) return false;
|
||||
if (this.pendingExits.length === 0) {
|
||||
console.log("No more exits available. Dungeon generation stopped.");
|
||||
this.isComplete = true;
|
||||
/**
|
||||
* Player selects a door to expand from
|
||||
*/
|
||||
selectDoor(exitPoint) {
|
||||
console.log('[DungeonGenerator] selectDoor called with:', exitPoint);
|
||||
console.log('[DungeonGenerator] Current state:', this.state);
|
||||
console.log('[DungeonGenerator] Available exits:', this.availableExits);
|
||||
|
||||
if (this.state !== PLACEMENT_STATE.WAITING_DOOR) {
|
||||
console.warn("Not in door selection mode");
|
||||
return false;
|
||||
}
|
||||
|
||||
const card = this.deck.draw();
|
||||
if (!card) {
|
||||
console.log("Deck empty. Dungeon complete.");
|
||||
this.isComplete = true;
|
||||
if (!exitPoint) {
|
||||
console.error("exitPoint is undefined!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// We process exits in groups now?
|
||||
// Or simply: When we pick an exit, we verify if it is part of a larger door.
|
||||
// Actually, 'pendingExits' contains individual cells.
|
||||
// Let's pick one.
|
||||
const targetExit = this.pendingExits.shift();
|
||||
// Validate exit exists
|
||||
const exitExists = this.availableExits.some(
|
||||
e => e.x === exitPoint.x && e.y === exitPoint.y && e.direction === exitPoint.direction
|
||||
);
|
||||
|
||||
// 1. Identify the "Global Reference Point" for the door this exit belongs to.
|
||||
// (If door is 2-wide, we want the One with the LOWEST X or LOWEST Y).
|
||||
// WE MUST FIND ITS SIBLING if it exists in 'pendingExits'.
|
||||
// This stops us from trying to attach a door twice (once per cell).
|
||||
|
||||
// Simple heuristic: If we have an exit at (x,y), check (x+1,y) or (x,y+1) depending on dir.
|
||||
// If the sibling is also in pendingExits, we effectively "consume" it too for this placement.
|
||||
|
||||
// Better: Find the "Left-Most" or "Bottom-Most" cell of this specific connection interface.
|
||||
// And use THAT as the target.
|
||||
|
||||
const targetRef = this.findExitReference(targetExit);
|
||||
console.log(`Attempting to place ${card.name} at Global Ref ${targetRef.x},${targetRef.y} (${targetRef.direction})`);
|
||||
|
||||
const requiredInputDirection = this.getOppositeDirection(targetRef.direction);
|
||||
let placed = false;
|
||||
|
||||
// Try to fit the card
|
||||
// We iterate input exits on the NEW card.
|
||||
// We only look at "Reference" exits on the new card too (min x/y) to avoid duplicate attempts.
|
||||
const candidateExits = this.UniqueExits(card);
|
||||
|
||||
for (const candidateExit of candidateExits) {
|
||||
|
||||
const rotation = this.calculateRequiredRotation(candidateExit.direction, requiredInputDirection);
|
||||
|
||||
// Now calculate ALIGNMENT.
|
||||
// We want the "Min Cell" of the Candidate Door (after rotation)
|
||||
// To overlap with the "Neighbor Cell" of the "Min Cell" of the Target Door?
|
||||
// NO.
|
||||
// Target Door Min Cell is at (TX, TY).
|
||||
// Its "Connection Neighbor" is at (NX, NY).
|
||||
// We want Candidate Door (Rotated) Min Cell to be at (NX, NY).
|
||||
|
||||
// 1. Calculate the offset of Candidate 'Min Cell' relative to Tile Origin (0,0) AFTER rotation.
|
||||
const rotatedOffset = this.getRotatedOffset(candidateExit, rotation);
|
||||
|
||||
// 2. Calculate the global connection point input
|
||||
const connectionPoint = this.getNeighborCell(targetRef.x, targetRef.y, targetRef.direction);
|
||||
|
||||
// 3. Tile Position
|
||||
const posX = connectionPoint.x - rotatedOffset.x;
|
||||
const posY = connectionPoint.y - rotatedOffset.y;
|
||||
|
||||
if (this.grid.canPlace(card, posX, posY, rotation)) {
|
||||
// Success
|
||||
const newInstance = {
|
||||
id: `tile_${this.placedTiles.length}_${card.id}`,
|
||||
defId: card.id,
|
||||
x: posX,
|
||||
y: posY,
|
||||
rotation: rotation
|
||||
};
|
||||
|
||||
this.grid.placeTile(newInstance, card);
|
||||
this.placedTiles.push(newInstance);
|
||||
|
||||
// Add NEW exits
|
||||
this.addExitsToQueue(newInstance, card);
|
||||
|
||||
// Cleanup: Remove the used exit(s) from pendingExits
|
||||
// We used targetRef. We must also remove its sibling if it exists.
|
||||
// Or simply: filter out any pending exit that is now blocked.
|
||||
this.cleanupPendingExits();
|
||||
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
if (!exitExists) {
|
||||
console.warn("Invalid exit selected");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!placed) {
|
||||
console.log(`Could not fit ${card.name}. Discarding.`);
|
||||
// If failed, return the exit to the pool?
|
||||
// Or discard the exit as "Dead End"?
|
||||
// For now, put it back at the end of queue.
|
||||
this.pendingExits.push(targetExit);
|
||||
this.selectedExit = exitPoint;
|
||||
|
||||
// Draw next card
|
||||
this.currentCard = this.deck.draw();
|
||||
if (!this.currentCard) {
|
||||
console.log("Deck empty - dungeon complete");
|
||||
this.state = PLACEMENT_STATE.COMPLETE;
|
||||
this.notifyStateChange();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate initial placement position (3 units above the connection point)
|
||||
const connectionPoint = this.neighbor(exitPoint.x, exitPoint.y, exitPoint.direction);
|
||||
|
||||
// Start with NORTH rotation
|
||||
this.placementRotation = DIRECTIONS.NORTH;
|
||||
const variant = this.currentCard.variants[this.placementRotation];
|
||||
|
||||
// Find the exit on the new tile that should connect to selectedExit
|
||||
const requiredDirection = this.opposite(exitPoint.direction);
|
||||
const matchingExits = variant.exits.filter(e => e.direction === requiredDirection);
|
||||
|
||||
if (matchingExits.length > 0) {
|
||||
// Use first matching exit as anchor
|
||||
const anchor = matchingExits[0];
|
||||
this.placementX = Math.round(connectionPoint.x - anchor.x);
|
||||
this.placementY = Math.round(connectionPoint.y - anchor.y);
|
||||
} else {
|
||||
// Fallback: center on connection point
|
||||
this.placementX = Math.round(connectionPoint.x);
|
||||
this.placementY = Math.round(connectionPoint.y);
|
||||
}
|
||||
|
||||
this.state = PLACEMENT_STATE.PLACING_TILE;
|
||||
this.notifyPlacementUpdate();
|
||||
this.notifyStateChange();
|
||||
|
||||
console.log(`📦 Placing ${this.currentCard.name} at (${this.placementX}, ${this.placementY})`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
getNeighborCell(x, y, dir) {
|
||||
switch (dir) {
|
||||
case DIRECTIONS.NORTH: return { x: x, y: y + 1 };
|
||||
case DIRECTIONS.SOUTH: return { x: x, y: y - 1 };
|
||||
case DIRECTIONS.EAST: return { x: x + 1, y: y };
|
||||
case DIRECTIONS.WEST: return { x: x - 1, y: y };
|
||||
}
|
||||
/**
|
||||
* Rotate placement tile 90° clockwise
|
||||
*/
|
||||
rotatePlacement() {
|
||||
if (this.state !== PLACEMENT_STATE.PLACING_TILE) return;
|
||||
|
||||
const rotations = [DIRECTIONS.NORTH, DIRECTIONS.EAST, DIRECTIONS.SOUTH, DIRECTIONS.WEST];
|
||||
const currentIndex = rotations.indexOf(this.placementRotation);
|
||||
this.placementRotation = rotations[(currentIndex + 1) % 4];
|
||||
|
||||
console.log(`🔄 Rotated to ${this.placementRotation}`);
|
||||
this.notifyPlacementUpdate();
|
||||
}
|
||||
|
||||
findExitReference(exit) {
|
||||
// If facing North/South, Reference is Minimum X.
|
||||
// If facing East/West, Reference is Minimum Y.
|
||||
/**
|
||||
* Move placement tile by offset
|
||||
*/
|
||||
movePlacement(dx, dy) {
|
||||
if (this.state !== PLACEMENT_STATE.PLACING_TILE) return;
|
||||
|
||||
// This function assumes 'exit' is from pendingExits (Global coords).
|
||||
// It checks if there is a "Lower" sibling also in pendingExits.
|
||||
// If so, returns the lower sibling. BEFORE using this exit.
|
||||
this.placementX += dx;
|
||||
this.placementY += dy;
|
||||
|
||||
let bestExit = exit;
|
||||
|
||||
// Check for siblings in pendingExits that match direction and are < coordinate
|
||||
// This is O(N) but N is small.
|
||||
for (const other of this.pendingExits) {
|
||||
if (other === exit) continue;
|
||||
if (other.direction !== exit.direction) continue;
|
||||
|
||||
if (exit.direction === DIRECTIONS.NORTH || exit.direction === DIRECTIONS.SOUTH) {
|
||||
// Check X. Adjacent implies y same, x diff 1.
|
||||
if (other.y === exit.y && Math.abs(other.x - exit.x) === 1) {
|
||||
if (other.x < bestExit.x) bestExit = other;
|
||||
}
|
||||
} else {
|
||||
// Check Y. adjacent implies x same, y diff 1.
|
||||
if (other.x === exit.x && Math.abs(other.y - exit.y) === 1) {
|
||||
if (other.y < bestExit.y) bestExit = other;
|
||||
}
|
||||
}
|
||||
}
|
||||
return bestExit;
|
||||
console.log(`↔️ Moved to (${this.placementX}, ${this.placementY})`);
|
||||
this.notifyPlacementUpdate();
|
||||
}
|
||||
|
||||
UniqueExits(tileDef) {
|
||||
// Filter tileDef.exits to only return the "Reference" (Min x/y) for each face/group.
|
||||
// This prevents trying to attach the same door 2 times.
|
||||
const unique = [];
|
||||
const seen = new Set(); // store "dir_coord" keys
|
||||
/**
|
||||
* Check if current placement is valid
|
||||
*/
|
||||
isPlacementValid() {
|
||||
if (!this.currentCard || this.state !== PLACEMENT_STATE.PLACING_TILE) return false;
|
||||
|
||||
// Sort exits to ensure we find Min first
|
||||
const sorted = [...tileDef.exits].sort((a, b) => {
|
||||
if (a.direction !== b.direction) return a.direction.localeCompare(b.direction);
|
||||
if (a.x !== b.x) return a.x - b.x;
|
||||
return a.y - b.y;
|
||||
});
|
||||
|
||||
for (const ex of sorted) {
|
||||
// Identifier for the "Door Group".
|
||||
// If North/South: ID is "Dir_Y". (X varies)
|
||||
// If East/West: ID is "Dir_X". (Y varies)
|
||||
// Actually, we just need to pick the first one we see (since we sorted by X then Y).
|
||||
// If we have (0,0) and (1,0) for SOUTH. Sorted -> (0,0) comes first.
|
||||
// We take (0,0). We assume (1,0) is part of same door.
|
||||
|
||||
// Heuristic: If this exit is adjacent to the last added unique exit of same direction, skip it.
|
||||
const last = unique[unique.length - 1];
|
||||
let isSameDoor = false;
|
||||
|
||||
if (last && last.direction === ex.direction) {
|
||||
if (ex.direction === DIRECTIONS.NORTH || ex.direction === DIRECTIONS.SOUTH) {
|
||||
// Vertical door, check horizontal adjacency
|
||||
if (last.y === ex.y && Math.abs(last.x - ex.x) <= 1) isSameDoor = true;
|
||||
} else {
|
||||
// Horizontal door, check vertical adjacency
|
||||
if (last.x === ex.x && Math.abs(last.y - ex.y) <= 1) isSameDoor = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSameDoor) {
|
||||
unique.push(ex);
|
||||
}
|
||||
}
|
||||
return unique;
|
||||
const variant = this.currentCard.variants[this.placementRotation];
|
||||
return this.grid.canPlace(variant, this.placementX, this.placementY);
|
||||
}
|
||||
|
||||
getRotatedOffset(localExit, rotation) {
|
||||
// Calculate where the 'localExit' ends up relative to (0,0) after rotation.
|
||||
// localExit is the "Reference" (Min) of the candidate door.
|
||||
|
||||
let rx, ry;
|
||||
const lx = localExit.x;
|
||||
const ly = localExit.y;
|
||||
|
||||
switch (rotation) {
|
||||
case DIRECTIONS.NORTH: rx = lx; ry = ly; break;
|
||||
case DIRECTIONS.SOUTH: rx = -lx; ry = -ly; break;
|
||||
case DIRECTIONS.EAST: rx = ly; ry = -lx; break;
|
||||
case DIRECTIONS.WEST: rx = -ly; ry = lx; break;
|
||||
/**
|
||||
* Confirm and finalize tile placement
|
||||
*/
|
||||
confirmPlacement() {
|
||||
if (this.state !== PLACEMENT_STATE.PLACING_TILE) {
|
||||
console.warn("Not in placement mode");
|
||||
return false;
|
||||
}
|
||||
|
||||
return { x: rx, y: ry };
|
||||
}
|
||||
|
||||
getOppositeDirection(dir) {
|
||||
switch (dir) {
|
||||
case DIRECTIONS.NORTH: return DIRECTIONS.SOUTH;
|
||||
case DIRECTIONS.SOUTH: return DIRECTIONS.NORTH;
|
||||
case DIRECTIONS.EAST: return DIRECTIONS.WEST;
|
||||
case DIRECTIONS.WEST: return DIRECTIONS.EAST;
|
||||
if (!this.isPlacementValid()) {
|
||||
console.warn("❌ Cannot place tile - invalid position");
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`[confirmPlacement] Placing at (${this.placementX}, ${this.placementY}) rotation: ${this.placementRotation}`);
|
||||
|
||||
// Round to integers (tiles must be on grid cells)
|
||||
const finalX = Math.round(this.placementX);
|
||||
const finalY = Math.round(this.placementY);
|
||||
|
||||
console.log(`[confirmPlacement] Rounded to (${finalX}, ${finalY})`);
|
||||
|
||||
// Place the tile
|
||||
this.placeCardFinal(
|
||||
this.currentCard,
|
||||
finalX,
|
||||
finalY,
|
||||
this.placementRotation
|
||||
);
|
||||
|
||||
// Reset placement state
|
||||
this.currentCard = null;
|
||||
this.selectedExit = null;
|
||||
this.state = PLACEMENT_STATE.WAITING_DOOR;
|
||||
|
||||
this.notifyPlacementUpdate(); // Clear preview
|
||||
this.notifyStateChange();
|
||||
console.log("✅ Tile placed successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
calculateRequiredRotation(localDir, targetGlobalDir) {
|
||||
const dirs = [DIRECTIONS.NORTH, DIRECTIONS.EAST, DIRECTIONS.SOUTH, DIRECTIONS.WEST];
|
||||
const localIdx = dirs.indexOf(localDir);
|
||||
const targetIdx = dirs.indexOf(targetGlobalDir);
|
||||
const diff = (targetIdx - localIdx + 4) % 4;
|
||||
return dirs[diff];
|
||||
/**
|
||||
* Internal: Actually place a card on the grid
|
||||
*/
|
||||
placeCardFinal(card, x, y, rotation) {
|
||||
console.log('[placeCardFinal] Card:', card);
|
||||
console.log('[placeCardFinal] Card.variants:', card.variants);
|
||||
console.log('[placeCardFinal] Rotation:', rotation, 'Type:', typeof rotation);
|
||||
|
||||
const variant = card.variants[rotation];
|
||||
console.log('[placeCardFinal] Variant:', variant);
|
||||
|
||||
const instance = {
|
||||
id: `tile_${this.placedTiles.length}`,
|
||||
defId: card.id,
|
||||
x, y, rotation,
|
||||
name: card.name
|
||||
};
|
||||
|
||||
this.grid.placeTile(instance, variant, card);
|
||||
this.placedTiles.push(instance);
|
||||
|
||||
// Update available exits
|
||||
this.updateAvailableExits(instance, variant, x, y);
|
||||
}
|
||||
|
||||
addExitsToQueue(tileInstance, tileDef) {
|
||||
for (const exit of tileDef.exits) {
|
||||
const globalPoint = this.grid.getGlobalPoint(exit.x, exit.y, tileInstance);
|
||||
const globalDir = this.grid.getRotatedDirection(exit.direction, tileInstance.rotation);
|
||||
/**
|
||||
* Update list of exits player can choose from
|
||||
*/
|
||||
updateAvailableExits(instance, variant, anchorX, anchorY) {
|
||||
console.log('[updateAvailableExits] ===== NUEVO CODIGO ===== Called for tile:', instance.id);
|
||||
console.log('[updateAvailableExits] Variant exits:', variant.exits);
|
||||
console.log('[updateAvailableExits] Anchor:', anchorX, anchorY);
|
||||
|
||||
// Check if blocked immediately
|
||||
const neighbor = this.getNeighborCell(globalPoint.x, globalPoint.y, globalDir);
|
||||
const key = `${neighbor.x},${neighbor.y}`;
|
||||
// Add new exits from this tile
|
||||
for (const ex of variant.exits) {
|
||||
const gx = anchorX + ex.x;
|
||||
const gy = anchorY + ex.y;
|
||||
|
||||
if (!this.grid.occupiedCells.has(key)) {
|
||||
this.pendingExits.push({
|
||||
x: globalPoint.x,
|
||||
y: globalPoint.y,
|
||||
direction: globalDir
|
||||
const leadingTo = this.neighbor(gx, gy, ex.direction);
|
||||
const isOccupied = this.grid.isOccupied(leadingTo.x, leadingTo.y);
|
||||
|
||||
console.log(`[updateAvailableExits] Exit at (${gx}, ${gy}) dir ${ex.direction} -> leads to (${leadingTo.x}, ${leadingTo.y}) occupied: ${isOccupied}`);
|
||||
|
||||
if (!isOccupied) {
|
||||
this.availableExits.push({
|
||||
x: gx,
|
||||
y: gy,
|
||||
direction: ex.direction,
|
||||
tileId: instance.id
|
||||
});
|
||||
console.log('[updateAvailableExits] ✓ Added exit');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[updateAvailableExits] Total available exits now:', this.availableExits.length);
|
||||
|
||||
// Remove exits that are now blocked or connected
|
||||
this.availableExits = this.availableExits.filter(exit => {
|
||||
const leadingTo = this.neighbor(exit.x, exit.y, exit.direction);
|
||||
return !this.grid.isOccupied(leadingTo.x, leadingTo.y);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current placement preview data for renderer
|
||||
*/
|
||||
getPlacementPreview() {
|
||||
if (this.state !== PLACEMENT_STATE.PLACING_TILE || !this.currentCard) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const variant = this.currentCard.variants[this.placementRotation];
|
||||
const cells = this.grid.calculateCells(variant, this.placementX, this.placementY);
|
||||
const isValid = this.isPlacementValid();
|
||||
|
||||
return {
|
||||
card: this.currentCard,
|
||||
rotation: this.placementRotation,
|
||||
x: this.placementX,
|
||||
y: this.placementY,
|
||||
cells,
|
||||
isValid,
|
||||
variant
|
||||
};
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
notifyStateChange() {
|
||||
if (this.onStateChange) {
|
||||
this.onStateChange(this.state);
|
||||
}
|
||||
}
|
||||
|
||||
cleanupPendingExits() {
|
||||
// Remove exits that now point to occupied cells (blocked by newly placed tile)
|
||||
this.pendingExits = this.pendingExits.filter(ex => {
|
||||
const neighbor = this.getNeighborCell(ex.x, ex.y, ex.direction);
|
||||
const key = `${neighbor.x},${neighbor.y}`;
|
||||
return !this.grid.occupiedCells.has(key);
|
||||
});
|
||||
notifyPlacementUpdate() {
|
||||
if (this.onPlacementUpdate) {
|
||||
const preview = this.getPlacementPreview();
|
||||
this.onPlacementUpdate(preview);
|
||||
}
|
||||
}
|
||||
|
||||
neighbor(x, y, dir) {
|
||||
switch (dir) {
|
||||
case DIRECTIONS.NORTH: return { x, y: y + 1 };
|
||||
case DIRECTIONS.SOUTH: return { x, y: y - 1 };
|
||||
case DIRECTIONS.EAST: return { x: x + 1, y };
|
||||
case DIRECTIONS.WEST: return { x: x - 1, y };
|
||||
}
|
||||
}
|
||||
|
||||
opposite(dir) {
|
||||
const map = {
|
||||
[DIRECTIONS.NORTH]: DIRECTIONS.SOUTH,
|
||||
[DIRECTIONS.SOUTH]: DIRECTIONS.NORTH,
|
||||
[DIRECTIONS.EAST]: DIRECTIONS.WEST,
|
||||
[DIRECTIONS.WEST]: DIRECTIONS.EAST
|
||||
};
|
||||
return map[dir];
|
||||
}
|
||||
}
|
||||
|
||||
50
src/engine/dungeon/DungeonGenerator.js.temp
Normal file
50
src/engine/dungeon/DungeonGenerator.js.temp
Normal file
@@ -0,0 +1,50 @@
|
||||
|
||||
tryPlaceCard(card, targetExit) {
|
||||
const requiredDirection = this.opposite(targetExit.direction);
|
||||
const targetCell = this.neighbor(targetExit.x, targetExit.y, targetExit.direction);
|
||||
|
||||
const rotations = [DIRECTIONS.NORTH, DIRECTIONS.EAST, DIRECTIONS.SOUTH, DIRECTIONS.WEST];
|
||||
// const shuffled = this.shuffle(rotations);
|
||||
|
||||
// 🔧 DEBUG: Force SOUTH rotation only
|
||||
const forcedRotations = [DIRECTIONS.SOUTH];
|
||||
console.log('⚠️ FORCED ROTATION TO SOUTH ONLY FOR TESTING');
|
||||
|
||||
let bestPlacement = null;
|
||||
let maxConnections = -1;
|
||||
|
||||
for (const rotation of forcedRotations) {
|
||||
const rotatedExits = this.rotateExits(card.exits, rotation);
|
||||
const candidates = rotatedExits.filter(e => e.direction === requiredDirection);
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const anchorX = targetCell.x - candidate.x;
|
||||
const anchorY = targetCell.y - candidate.y;
|
||||
|
||||
if (this.grid.canPlace(card, anchorX, anchorY, rotation)) {
|
||||
let score = 0;
|
||||
for (const exit of rotatedExits) {
|
||||
const globalX = anchorX + exit.x;
|
||||
const globalY = anchorY + exit.y;
|
||||
const neighbor = this.neighbor(globalX, globalY, exit.direction);
|
||||
if (this.grid.occupiedCells.has(`${neighbor.x},${neighbor.y}`)) {
|
||||
score++;
|
||||
}
|
||||
}
|
||||
|
||||
if (score > maxConnections) {
|
||||
maxConnections = score;
|
||||
bestPlacement = { card, anchorX, anchorY, rotation };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestPlacement) {
|
||||
this.placeTileAt(bestPlacement.card, bestPlacement.anchorX, bestPlacement.anchorY, bestPlacement.rotation);
|
||||
console.log(`✅ Best Placement Selected: Score ${maxConnections}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -1,184 +1,106 @@
|
||||
import { DIRECTIONS } from './Constants.js';
|
||||
|
||||
export class GridSystem {
|
||||
/**
|
||||
* The GridSystem maintains the "Source of Truth" for the dungeon layout.
|
||||
* It knows which cells are occupied and by whom.
|
||||
* Dependencies: Constants.js (DIRECTIONS)
|
||||
*/
|
||||
constructor() {
|
||||
// We use a Map for O(1) lookups.
|
||||
// Key: "x,y" (String) -> Value: "tileId" (String)
|
||||
// Map "x,y" -> "tileId"
|
||||
this.occupiedCells = new Map();
|
||||
|
||||
// We also keep a list of placed tile objects for easier iteration if needed later.
|
||||
this.tiles = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a tile can be placed at the given coordinates with the given rotation.
|
||||
* Needs: The Tile Definition (to know size), the target X,Y, and desired Rotation.
|
||||
* Checks if a specific VARIANT can be placed at anchorX, anchorY.
|
||||
* Does NOT rotate anything. Assumes variant is already the correct shape.
|
||||
*/
|
||||
canPlace(tileDef, startX, startY, rotation) {
|
||||
// 1. Calculate the real-world coordinates of every single cell this tile would occupy.
|
||||
const cells = this.getGlobalCells(tileDef, startX, startY, rotation);
|
||||
canPlace(variant, anchorX, anchorY) {
|
||||
const layout = variant.layout;
|
||||
const rows = layout.length;
|
||||
|
||||
// 2. Check each cell against our Map of occupied spots.
|
||||
for (const cell of cells) {
|
||||
const key = `${cell.x},${cell.y}`;
|
||||
if (this.occupiedCells.has(key)) {
|
||||
return false; // COLLISION! Spot already taken.
|
||||
for (let row = 0; row < rows; row++) {
|
||||
const rowData = layout[row];
|
||||
const cols = rowData.length;
|
||||
|
||||
for (let col = 0; col < cols; col++) {
|
||||
// If cell is 0 (empty), skip
|
||||
if (rowData[col] === 0) continue;
|
||||
|
||||
// Calculate Global Position
|
||||
// Matrix Row 0 is Top (Max Y). Matrix Row Max is Bottom (Y=0).
|
||||
const lx = col;
|
||||
const ly = (rows - 1) - row;
|
||||
|
||||
const gx = anchorX + lx;
|
||||
const gy = anchorY + ly;
|
||||
const key = `${gx},${gy}`;
|
||||
|
||||
// Collision Check
|
||||
if (this.occupiedCells.has(key)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true; // All clear.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Officially registers a tile onto the board.
|
||||
* Should only be called AFTER canPlace returns true.
|
||||
* Registers the tile cells as occupied.
|
||||
*/
|
||||
placeTile(tileInstance, tileDef) {
|
||||
const cells = this.getGlobalCells(tileDef, tileInstance.x, tileInstance.y, tileInstance.rotation);
|
||||
placeTile(tileInstance, variant) {
|
||||
const layout = variant.layout;
|
||||
const rows = layout.length;
|
||||
const anchorX = tileInstance.x;
|
||||
const anchorY = tileInstance.y;
|
||||
|
||||
// Record every cell in our Map
|
||||
for (const cell of cells) {
|
||||
const key = `${cell.x},${cell.y}`;
|
||||
this.occupiedCells.set(key, tileInstance.id);
|
||||
}
|
||||
|
||||
// Store the instance
|
||||
this.tiles.push(tileInstance);
|
||||
console.log(`Placed tile ${tileInstance.id} at ${tileInstance.x},${tileInstance.y}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* THE MAGIC MATH FUNCTION.
|
||||
* Converts a simplified abstract tile (width/length) into actual grid coordinates.
|
||||
* Handles the Rotation logic (N, S, E, W).
|
||||
* NOW SUPPORTS: Matrix Layouts (0 = Empty).
|
||||
*/
|
||||
getGlobalCells(tileDef, startX, startY, rotation) {
|
||||
const cells = [];
|
||||
const layout = tileDef.layout;
|
||||
|
||||
// Safety check: if no layout, fallback to full rectangle (optional, but good for stability)
|
||||
// usage: const w = tileDef.width; const l = tileDef.length;
|
||||
|
||||
if (!layout) {
|
||||
console.error("Tile definition missing layout. ID:", tileDef?.id);
|
||||
console.warn("Invalid tileDef object:", tileDef);
|
||||
return cells;
|
||||
}
|
||||
|
||||
const numberOfRows = layout.length; // usually equals tileDef.length
|
||||
|
||||
// Iterate through matrix rows
|
||||
for (let row = 0; row < numberOfRows; row++) {
|
||||
for (let row = 0; row < rows; row++) {
|
||||
const rowData = layout[row];
|
||||
const numberOfCols = rowData.length; // usually equals tileDef.width
|
||||
const cols = rowData.length;
|
||||
|
||||
for (let col = 0; col < numberOfCols; col++) {
|
||||
const cellValue = rowData[col];
|
||||
|
||||
// CRITICAL: Skip empty cells (0)
|
||||
if (cellValue === 0) continue;
|
||||
|
||||
// Map Matrix (Row, Col) to Local Grid (lx, ly)
|
||||
// Matrix Row 0 is the "Top" (Max Y).
|
||||
// Matrix Row (Rows-1) is the "Bottom" (Y=0).
|
||||
// So: ly = (numberOfRows - 1) - row
|
||||
// lx = col
|
||||
for (let col = 0; col < cols; col++) {
|
||||
if (rowData[col] === 0) continue;
|
||||
|
||||
const lx = col;
|
||||
const ly = (numberOfRows - 1) - row;
|
||||
const ly = (rows - 1) - row;
|
||||
|
||||
let gx, gy;
|
||||
const gx = anchorX + lx;
|
||||
const gy = anchorY + ly;
|
||||
const key = `${gx},${gy}`;
|
||||
|
||||
// Apply Rotation to the local (lx, ly) point relative to (0,0) anchor
|
||||
switch (rotation) {
|
||||
case DIRECTIONS.NORTH:
|
||||
// Standard: +X is Right, +Y is Forward
|
||||
gx = startX + lx;
|
||||
gy = startY + ly;
|
||||
break;
|
||||
case DIRECTIONS.SOUTH:
|
||||
// 180 degrees: Extension goes "Backwards" and "Leftwards" relative to pivot
|
||||
gx = startX - lx;
|
||||
gy = startY - ly;
|
||||
break;
|
||||
case DIRECTIONS.EAST:
|
||||
// 90 degrees Clockwise: Width becomes "Length", Length becomes "Width"
|
||||
// x' = y, y' = -x
|
||||
gx = startX + ly;
|
||||
gy = startY - lx;
|
||||
break;
|
||||
case DIRECTIONS.WEST:
|
||||
// 270 degrees Clockwise (or 90 Counter-Clockwise)
|
||||
// x' = -y, y' = x
|
||||
gx = startX - ly;
|
||||
gy = startY + lx;
|
||||
break;
|
||||
default:
|
||||
gx = startX + lx;
|
||||
gy = startY + ly;
|
||||
}
|
||||
this.occupiedCells.set(key, tileInstance.id);
|
||||
}
|
||||
}
|
||||
this.tiles.push(tileInstance);
|
||||
console.log(`[Grid] Placed ${tileInstance.id} at ${anchorX},${anchorY} (Rot: ${tileInstance.rotation})`);
|
||||
}
|
||||
|
||||
// We could also store the 'cellValue' (height) if we wanted.
|
||||
cells.push({ x: gx, y: gy, value: cellValue });
|
||||
/**
|
||||
* Helper to return global cells for logic/renderer without modifying state.
|
||||
*/
|
||||
calculateCells(variant, anchorX, anchorY) {
|
||||
const cells = [];
|
||||
const layout = variant.layout;
|
||||
const rows = layout.length;
|
||||
|
||||
for (let row = 0; row < rows; row++) {
|
||||
const rowData = layout[row];
|
||||
const cols = rowData.length;
|
||||
|
||||
for (let col = 0; col < cols; col++) {
|
||||
if (rowData[col] === 0) continue;
|
||||
|
||||
const lx = col;
|
||||
const ly = (rows - 1) - row;
|
||||
|
||||
const gx = anchorX + lx;
|
||||
const gy = anchorY + ly;
|
||||
|
||||
cells.push({ x: gx, y: gy, value: rowData[col] });
|
||||
}
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a local point (like an exit definition) to Global Coordinates.
|
||||
* Useful for calculating where an exit actually ends up on the board.
|
||||
* Helper to see if a specific global coordinate is occupied
|
||||
*/
|
||||
getGlobalPoint(localX, localY, tileInstance) {
|
||||
let gx, gy;
|
||||
const startX = tileInstance.x;
|
||||
const startY = tileInstance.y;
|
||||
const rotation = tileInstance.rotation;
|
||||
|
||||
switch (rotation) {
|
||||
case DIRECTIONS.NORTH:
|
||||
gx = startX + localX;
|
||||
gy = startY + localY;
|
||||
break;
|
||||
case DIRECTIONS.SOUTH:
|
||||
gx = startX - localX;
|
||||
gy = startY - localY;
|
||||
break;
|
||||
case DIRECTIONS.EAST:
|
||||
gx = startX + localY;
|
||||
gy = startY - localX;
|
||||
break;
|
||||
case DIRECTIONS.WEST:
|
||||
gx = startX - localY;
|
||||
gy = startY + localX;
|
||||
break;
|
||||
default:
|
||||
gx = startX + localX;
|
||||
gy = startY + localY;
|
||||
}
|
||||
return { x: gx, y: gy };
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates a direction (N, S, E, W) by a given amount.
|
||||
* Useful for calculating which way an exit faces after the tile is rotated.
|
||||
*/
|
||||
getRotatedDirection(originalDirection, tileRotation) {
|
||||
// N=0, E=1, S=2, W=3
|
||||
const dirs = [DIRECTIONS.NORTH, DIRECTIONS.EAST, DIRECTIONS.SOUTH, DIRECTIONS.WEST];
|
||||
const idx = dirs.indexOf(originalDirection);
|
||||
|
||||
let rotationSteps = 0;
|
||||
if (tileRotation === DIRECTIONS.EAST) rotationSteps = 1;
|
||||
if (tileRotation === DIRECTIONS.SOUTH) rotationSteps = 2;
|
||||
if (tileRotation === DIRECTIONS.WEST) rotationSteps = 3;
|
||||
|
||||
const newIdx = (idx + rotationSteps) % 4;
|
||||
return dirs[newIdx];
|
||||
isOccupied(x, y) {
|
||||
return this.occupiedCells.has(`${x},${y}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,151 +1,379 @@
|
||||
|
||||
import { DIRECTIONS, TILE_TYPES } from './Constants.js';
|
||||
|
||||
export const TILES = [
|
||||
{
|
||||
export const TILES = {
|
||||
// -------------------------------------------------------------------------
|
||||
// CORRIDOR STRAIGHT
|
||||
// -------------------------------------------------------------------------
|
||||
'corridor_straight': {
|
||||
id: 'corridor_straight',
|
||||
name: 'Corridor',
|
||||
type: TILE_TYPES.CORRIDOR,
|
||||
width: 2,
|
||||
length: 6,
|
||||
textures: ['/assets/images/dungeon1/tiles/corridor1.png'],
|
||||
layout: [
|
||||
[1, 1], // y=5 (North)
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
[1, 1] // y=0 (South)
|
||||
],
|
||||
exits: [
|
||||
// South
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 1, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
// North
|
||||
{ x: 0, y: 5, direction: DIRECTIONS.NORTH },
|
||||
{ x: 1, y: 5, direction: DIRECTIONS.NORTH }
|
||||
]
|
||||
variants: {
|
||||
[DIRECTIONS.NORTH]: {
|
||||
width: 2, height: 6,
|
||||
layout: [[1, 1], [1, 1], [1, 1], [1, 1], [1, 1], [1, 1]],
|
||||
exits: [
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.SOUTH }, { x: 1, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 0, y: 5, direction: DIRECTIONS.NORTH }, { x: 1, y: 5, direction: DIRECTIONS.NORTH }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.SOUTH]: {
|
||||
width: 2, height: 6,
|
||||
layout: [[1, 1], [1, 1], [1, 1], [1, 1], [1, 1], [1, 1]],
|
||||
exits: [
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.SOUTH }, { x: 1, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 0, y: 5, direction: DIRECTIONS.NORTH }, { x: 1, y: 5, direction: DIRECTIONS.NORTH }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.EAST]: {
|
||||
width: 6, height: 2,
|
||||
layout: [[1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.WEST }, { x: 0, y: 1, direction: DIRECTIONS.WEST },
|
||||
{ x: 5, y: 0, direction: DIRECTIONS.EAST }, { x: 5, y: 1, direction: DIRECTIONS.EAST }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.WEST]: {
|
||||
width: 6, height: 2,
|
||||
layout: [[1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.WEST }, { x: 0, y: 1, direction: DIRECTIONS.WEST },
|
||||
{ x: 5, y: 0, direction: DIRECTIONS.EAST }, { x: 5, y: 1, direction: DIRECTIONS.EAST }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// CORRIDOR STEPS
|
||||
// -------------------------------------------------------------------------
|
||||
'corridor_steps': {
|
||||
id: 'corridor_steps',
|
||||
name: 'Steps',
|
||||
type: TILE_TYPES.CORRIDOR,
|
||||
width: 2,
|
||||
length: 6,
|
||||
textures: ['/assets/images/dungeon1/tiles/stairs1.png'],
|
||||
layout: [
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
[1, 1]
|
||||
],
|
||||
exits: [
|
||||
// South
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 1, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
// North
|
||||
{ x: 0, y: 5, direction: DIRECTIONS.NORTH },
|
||||
{ x: 1, y: 5, direction: DIRECTIONS.NORTH }
|
||||
]
|
||||
variants: {
|
||||
[DIRECTIONS.NORTH]: {
|
||||
width: 2, height: 6,
|
||||
layout: [[1, 1], [1, 1], [1, 1], [1, 1], [1, 1], [1, 1]],
|
||||
exits: [
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.SOUTH }, { x: 1, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 0, y: 5, direction: DIRECTIONS.NORTH }, { x: 1, y: 5, direction: DIRECTIONS.NORTH }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.SOUTH]: {
|
||||
width: 2, height: 6,
|
||||
layout: [[1, 1], [1, 1], [1, 1], [1, 1], [1, 1], [1, 1]],
|
||||
exits: [
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.SOUTH }, { x: 1, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 0, y: 5, direction: DIRECTIONS.NORTH }, { x: 1, y: 5, direction: DIRECTIONS.NORTH }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.EAST]: {
|
||||
width: 6, height: 2,
|
||||
layout: [[1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.WEST }, { x: 0, y: 1, direction: DIRECTIONS.WEST },
|
||||
{ x: 5, y: 0, direction: DIRECTIONS.EAST }, { x: 5, y: 1, direction: DIRECTIONS.EAST }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.WEST]: {
|
||||
width: 6, height: 2,
|
||||
layout: [[1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.WEST }, { x: 0, y: 1, direction: DIRECTIONS.WEST },
|
||||
{ x: 5, y: 0, direction: DIRECTIONS.EAST }, { x: 5, y: 1, direction: DIRECTIONS.EAST }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// CORNER (L-Shape)
|
||||
// -------------------------------------------------------------------------
|
||||
'corridor_corner': {
|
||||
id: 'corridor_corner',
|
||||
name: 'Corner',
|
||||
type: TILE_TYPES.CORRIDOR,
|
||||
width: 4,
|
||||
length: 4,
|
||||
textures: ['/assets/images/dungeon1/tiles/L.png'],
|
||||
layout: [
|
||||
[1, 1, 1, 1], // y=3 (Top)
|
||||
[1, 1, 1, 1], // y=2 (East Exit here at x=3)
|
||||
[1, 1, 0, 0], // y=1
|
||||
[1, 1, 0, 0] // y=0 (South Exit here at x=0,1)
|
||||
],
|
||||
exits: [
|
||||
// South
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 1, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
// East
|
||||
{ x: 3, y: 2, direction: DIRECTIONS.EAST },
|
||||
{ x: 3, y: 3, direction: DIRECTIONS.EAST }
|
||||
]
|
||||
variants: {
|
||||
[DIRECTIONS.NORTH]: {
|
||||
width: 4, height: 4,
|
||||
layout: [
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 0, 0],
|
||||
[1, 1, 0, 0]
|
||||
],
|
||||
exits: [
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.SOUTH }, { x: 1, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 3, y: 2, direction: DIRECTIONS.EAST }, { x: 3, y: 3, direction: DIRECTIONS.EAST }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.EAST]: {
|
||||
width: 4, height: 4,
|
||||
layout: [
|
||||
[1, 1, 0, 0],
|
||||
[1, 1, 0, 0],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1]
|
||||
],
|
||||
exits: [
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.SOUTH }, { x: 1, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 2, y: 3, direction: DIRECTIONS.NORTH }, { x: 3, y: 3, direction: DIRECTIONS.NORTH }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.SOUTH]: {
|
||||
width: 4, height: 4,
|
||||
layout: [
|
||||
[0, 0, 1, 1],
|
||||
[0, 0, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1]
|
||||
],
|
||||
exits: [
|
||||
{ x: 2, y: 3, direction: DIRECTIONS.NORTH }, { x: 3, y: 3, direction: DIRECTIONS.NORTH },
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.WEST }, { x: 0, y: 1, direction: DIRECTIONS.WEST }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.WEST]: {
|
||||
width: 4, height: 4,
|
||||
layout: [
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[0, 0, 1, 1],
|
||||
[0, 0, 1, 1]
|
||||
],
|
||||
exits: [
|
||||
{ x: 3, y: 2, direction: DIRECTIONS.EAST }, { x: 3, y: 3, direction: DIRECTIONS.EAST },
|
||||
{ x: 0, y: 1, direction: DIRECTIONS.WEST }, { x: 0, y: 0, direction: DIRECTIONS.WEST }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// T-JUNCTION
|
||||
// -------------------------------------------------------------------------
|
||||
'junction_t': {
|
||||
id: 'junction_t',
|
||||
name: 'T-Junction',
|
||||
type: TILE_TYPES.JUNCTION,
|
||||
width: 6,
|
||||
length: 4,
|
||||
textures: ['/assets/images/dungeon1/tiles/T.png'],
|
||||
layout: [
|
||||
[1, 1, 1, 1, 1, 1], // y=3
|
||||
[1, 1, 1, 1, 1, 1], // y=2 (West at x=0, East at x=5)
|
||||
[0, 0, 1, 1, 0, 0], // y=1
|
||||
[0, 0, 1, 1, 0, 0] // y=0 (South at x=2,3)
|
||||
],
|
||||
exits: [
|
||||
// South
|
||||
{ x: 2, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 3, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
// West
|
||||
{ x: 0, y: 2, direction: DIRECTIONS.WEST },
|
||||
{ x: 0, y: 3, direction: DIRECTIONS.WEST },
|
||||
// East
|
||||
{ x: 5, y: 2, direction: DIRECTIONS.EAST },
|
||||
{ x: 5, y: 3, direction: DIRECTIONS.EAST }
|
||||
]
|
||||
variants: {
|
||||
[DIRECTIONS.NORTH]: {
|
||||
width: 6, height: 4,
|
||||
layout: [
|
||||
[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: [
|
||||
{ x: 2, y: 0, direction: DIRECTIONS.SOUTH }, { x: 3, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 0, y: 2, direction: DIRECTIONS.WEST }, { x: 0, y: 3, direction: DIRECTIONS.WEST },
|
||||
{ x: 5, y: 2, direction: DIRECTIONS.EAST }, { x: 5, y: 3, direction: DIRECTIONS.EAST }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.EAST]: {
|
||||
width: 4, height: 6,
|
||||
layout: [
|
||||
[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: [
|
||||
{ x: 2, y: 0, direction: DIRECTIONS.SOUTH }, { x: 3, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 2, y: 5, direction: DIRECTIONS.NORTH }, { x: 3, y: 5, direction: DIRECTIONS.NORTH },
|
||||
{ x: 0, y: 2, direction: DIRECTIONS.WEST }, { x: 0, y: 3, direction: DIRECTIONS.WEST }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.SOUTH]: {
|
||||
width: 6, height: 4,
|
||||
layout: [
|
||||
[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: [
|
||||
{ x: 2, y: 3, direction: DIRECTIONS.NORTH }, { x: 3, y: 3, direction: DIRECTIONS.NORTH },
|
||||
{ x: 5, y: 0, direction: DIRECTIONS.EAST }, { x: 5, y: 1, direction: DIRECTIONS.EAST },
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.WEST }, { x: 0, y: 1, direction: DIRECTIONS.WEST }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.WEST]: {
|
||||
width: 4, height: 6,
|
||||
layout: [
|
||||
[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: [
|
||||
{ x: 3, y: 2, direction: DIRECTIONS.EAST }, { x: 3, y: 3, direction: DIRECTIONS.EAST },
|
||||
{ x: 0, y: 5, direction: DIRECTIONS.NORTH }, { x: 1, y: 5, direction: DIRECTIONS.NORTH },
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.SOUTH }, { x: 1, y: 0, direction: DIRECTIONS.SOUTH }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ROOM DUNGEON
|
||||
// -------------------------------------------------------------------------
|
||||
'room_dungeon': {
|
||||
id: 'room_dungeon',
|
||||
name: 'Dungeon Room',
|
||||
type: TILE_TYPES.ROOM,
|
||||
width: 4,
|
||||
length: 4,
|
||||
textures: [
|
||||
'/assets/images/dungeon1/tiles/room_4x4_circle.png',
|
||||
'/assets/images/dungeon1/tiles/room_4x4_orange.png',
|
||||
'/assets/images/dungeon1/tiles/room_4x4_squeleton.png'
|
||||
],
|
||||
layout: [
|
||||
[1, 1, 1, 1], // y=3 (North Exit at x=1,2)
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1] // y=0 (South Exit at x=1,2)
|
||||
],
|
||||
exits: [
|
||||
// South
|
||||
{ x: 1, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 2, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
// North
|
||||
{ x: 1, y: 3, direction: DIRECTIONS.NORTH },
|
||||
{ x: 2, y: 3, direction: DIRECTIONS.NORTH }
|
||||
]
|
||||
variants: {
|
||||
[DIRECTIONS.NORTH]: {
|
||||
width: 4, height: 4,
|
||||
layout: [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 1, y: 0, direction: DIRECTIONS.SOUTH }, { x: 2, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 1, y: 3, direction: DIRECTIONS.NORTH }, { x: 2, y: 3, direction: DIRECTIONS.NORTH }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.EAST]: {
|
||||
width: 4, height: 4,
|
||||
layout: [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 0, y: 1, direction: DIRECTIONS.WEST }, { x: 0, y: 2, direction: DIRECTIONS.WEST },
|
||||
{ x: 3, y: 1, direction: DIRECTIONS.EAST }, { x: 3, y: 2, direction: DIRECTIONS.EAST }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.SOUTH]: {
|
||||
width: 4, height: 4,
|
||||
layout: [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 1, y: 3, direction: DIRECTIONS.NORTH }, { x: 2, y: 3, direction: DIRECTIONS.NORTH },
|
||||
{ x: 1, y: 0, direction: DIRECTIONS.SOUTH }, { x: 2, y: 0, direction: DIRECTIONS.SOUTH }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.WEST]: {
|
||||
width: 4, height: 4,
|
||||
layout: [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 3, y: 1, direction: DIRECTIONS.EAST }, { x: 3, y: 2, direction: DIRECTIONS.EAST },
|
||||
{ x: 0, y: 1, direction: DIRECTIONS.WEST }, { x: 0, y: 2, direction: DIRECTIONS.WEST }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ROOM OBJECTIVE
|
||||
// -------------------------------------------------------------------------
|
||||
'room_objective': {
|
||||
id: 'room_objective',
|
||||
name: 'Dungeon Room',
|
||||
type: TILE_TYPES.ROOM,
|
||||
textures: [
|
||||
'/assets/images/dungeon1/tiles/room_4x4_circle.png',
|
||||
'/assets/images/dungeon1/tiles/room_4x4_orange.png',
|
||||
'/assets/images/dungeon1/tiles/room_4x4_squeleton.png'
|
||||
],
|
||||
variants: {
|
||||
[DIRECTIONS.NORTH]: {
|
||||
width: 4, height: 4,
|
||||
layout: [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 1, y: 0, direction: DIRECTIONS.SOUTH }, { x: 2, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 1, y: 3, direction: DIRECTIONS.NORTH }, { x: 2, y: 3, direction: DIRECTIONS.NORTH }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.EAST]: {
|
||||
width: 4, height: 4,
|
||||
layout: [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 0, y: 1, direction: DIRECTIONS.WEST }, { x: 0, y: 2, direction: DIRECTIONS.WEST },
|
||||
{ x: 3, y: 1, direction: DIRECTIONS.EAST }, { x: 3, y: 2, direction: DIRECTIONS.EAST }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.SOUTH]: {
|
||||
width: 4, height: 4,
|
||||
layout: [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 1, y: 3, direction: DIRECTIONS.NORTH }, { x: 2, y: 3, direction: DIRECTIONS.NORTH },
|
||||
{ x: 1, y: 0, direction: DIRECTIONS.SOUTH }, { x: 2, y: 0, direction: DIRECTIONS.SOUTH }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.WEST]: {
|
||||
width: 4, height: 4,
|
||||
layout: [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 3, y: 1, direction: DIRECTIONS.EAST }, { x: 3, y: 2, direction: DIRECTIONS.EAST },
|
||||
{ x: 0, y: 1, direction: DIRECTIONS.WEST }, { x: 0, y: 2, direction: DIRECTIONS.WEST }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ROOM OBJECTIVE
|
||||
// -------------------------------------------------------------------------
|
||||
'room_objective': {
|
||||
id: 'room_objective',
|
||||
name: 'Objective Room',
|
||||
type: TILE_TYPES.OBJECTIVE_ROOM,
|
||||
width: 4,
|
||||
length: 8,
|
||||
textures: [
|
||||
'/assets/images/dungeon1/tiles/room_4x8_altar.png',
|
||||
'/assets/images/dungeon1/tiles/room_4x8_tomb.png'
|
||||
],
|
||||
layout: [
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1] // South Exit
|
||||
],
|
||||
exits: [
|
||||
{ x: 1, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 2, y: 0, direction: DIRECTIONS.SOUTH }
|
||||
]
|
||||
variants: {
|
||||
[DIRECTIONS.NORTH]: {
|
||||
width: 4, height: 8,
|
||||
layout: [
|
||||
[1, 1, 1, 1], [1, 1, 1, 1], [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: [
|
||||
{ x: 1, y: 0, direction: DIRECTIONS.SOUTH }, { x: 2, y: 0, direction: DIRECTIONS.SOUTH }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.EAST]: {
|
||||
width: 8, height: 4,
|
||||
layout: [
|
||||
[1, 1, 1, 1, 1, 1, 1, 1], [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: [
|
||||
{ x: 0, y: 1, direction: DIRECTIONS.WEST }, { x: 0, y: 2, direction: DIRECTIONS.WEST }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.SOUTH]: {
|
||||
width: 4, height: 8,
|
||||
layout: [
|
||||
[1, 1, 1, 1], [1, 1, 1, 1], [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: [
|
||||
{ x: 1, y: 7, direction: DIRECTIONS.NORTH }, { x: 2, y: 7, direction: DIRECTIONS.NORTH }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.WEST]: {
|
||||
width: 8, height: 4,
|
||||
layout: [
|
||||
[1, 1, 1, 1, 1, 1, 1, 1], [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: [
|
||||
{ x: 7, y: 1, direction: DIRECTIONS.EAST }, { x: 7, y: 2, direction: DIRECTIONS.EAST }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
181
src/engine/dungeon/old/DungeonGenerator.js
Normal file
181
src/engine/dungeon/old/DungeonGenerator.js
Normal file
@@ -0,0 +1,181 @@
|
||||
import { DIRECTIONS } from './Constants.js';
|
||||
import { GridSystem } from './GridSystem.js';
|
||||
import { DungeonDeck } from './DungeonDeck.js';
|
||||
|
||||
export class DungeonGenerator {
|
||||
constructor() {
|
||||
this.grid = new GridSystem();
|
||||
this.deck = new DungeonDeck();
|
||||
this.pendingExits = [];
|
||||
this.placedTiles = [];
|
||||
this.isComplete = false;
|
||||
}
|
||||
|
||||
startDungeon(missionConfig) {
|
||||
const objectiveId = missionConfig?.type === 'quest' ? 'room_objective' : 'room_dungeon';
|
||||
this.deck.generateMissionDeck(objectiveId);
|
||||
|
||||
const firstCard = this.deck.draw();
|
||||
if (!firstCard) {
|
||||
console.error("❌ Deck empty!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Place first tile with NORTH variant
|
||||
this.placeTileAt(firstCard, 0, 0, DIRECTIONS.NORTH);
|
||||
console.log(`🏰 Dungeon started with ${firstCard.name}`);
|
||||
}
|
||||
|
||||
step() {
|
||||
if (this.isComplete || this.pendingExits.length === 0) {
|
||||
this.isComplete = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const card = this.deck.draw();
|
||||
if (!card) {
|
||||
this.isComplete = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetExit = this.pendingExits.shift();
|
||||
const placed = this.tryPlaceCard(card, targetExit);
|
||||
|
||||
if (!placed) {
|
||||
this.pendingExits.push(targetExit);
|
||||
console.warn(`⚠️ Failed to place ${card.name} - returning exit to pending queue`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
tryPlaceCard(card, targetExit) {
|
||||
console.log(`\n🃏 Trying to place card: ${card.name} (ID: ${card.id})`);
|
||||
|
||||
// Use standard opposite logic for Directions (String) since names don't change
|
||||
const requiredDirection = this.opposite(targetExit.direction);
|
||||
const targetCell = this.neighbor(targetExit.x, targetExit.y, targetExit.direction);
|
||||
|
||||
const rotations = [DIRECTIONS.NORTH, DIRECTIONS.EAST, DIRECTIONS.SOUTH, DIRECTIONS.WEST];
|
||||
const shuffled = this.shuffle(rotations);
|
||||
|
||||
let bestPlacement = null;
|
||||
let maxConnections = -1;
|
||||
|
||||
// Try ALL rotations
|
||||
for (const rotation of shuffled) {
|
||||
// 1. Get the pre-calculated VARIANT for this rotation
|
||||
const variant = card.variants[rotation];
|
||||
if (!variant) continue;
|
||||
|
||||
const variantExits = variant.exits;
|
||||
|
||||
// 2. Find candidate exits on this variant that face the required direction
|
||||
// Note: variant.exits are ALREADY correctly oriented for this rotation
|
||||
const candidates = variantExits.filter(e => e.direction === requiredDirection);
|
||||
|
||||
for (const candidate of candidates) {
|
||||
// Determine absolute anchor position
|
||||
// Since variant coords are all positive relative to (0,0), anchor calculation is simple subtraction
|
||||
const anchorX = targetCell.x - candidate.x;
|
||||
const anchorY = targetCell.y - candidate.y;
|
||||
|
||||
if (this.grid.canPlace(card, anchorX, anchorY, rotation)) {
|
||||
// Start Score at 0
|
||||
let score = 0;
|
||||
|
||||
// Calculate score
|
||||
for (const exit of variantExits) {
|
||||
const globalX = anchorX + exit.x;
|
||||
const globalY = anchorY + exit.y;
|
||||
|
||||
const neighbor = this.neighbor(globalX, globalY, exit.direction);
|
||||
const neighborKey = `${neighbor.x},${neighbor.y}`;
|
||||
|
||||
if (this.grid.occupiedCells.has(neighborKey)) {
|
||||
score++;
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer higher score.
|
||||
if (score > maxConnections) {
|
||||
maxConnections = score;
|
||||
bestPlacement = { card, anchorX, anchorY, rotation };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestPlacement) {
|
||||
this.placeTileAt(bestPlacement.card, bestPlacement.anchorX, bestPlacement.anchorY, bestPlacement.rotation);
|
||||
console.log(`✅ Placed ${card.name} at (${bestPlacement.anchorX}, ${bestPlacement.anchorY}) Rot:${bestPlacement.rotation} Score:${maxConnections}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`❌ Could not place ${card.name} in any rotation`);
|
||||
return false;
|
||||
}
|
||||
|
||||
placeTileAt(tileDef, x, y, rotation) {
|
||||
const instance = {
|
||||
id: `tile_${this.placedTiles.length}`,
|
||||
defId: tileDef.id,
|
||||
x, y, rotation
|
||||
};
|
||||
|
||||
this.grid.placeTile(instance, tileDef); // Grid system now handles looking up the variant
|
||||
this.placedTiles.push(instance);
|
||||
|
||||
// Add exits from the variant
|
||||
const variant = tileDef.variants[rotation];
|
||||
|
||||
for (const exit of variant.exits) {
|
||||
const globalX = x + exit.x;
|
||||
const globalY = y + exit.y;
|
||||
const neighborCell = this.neighbor(globalX, globalY, exit.direction);
|
||||
const key = `${neighborCell.x},${neighborCell.y}`;
|
||||
|
||||
// Only add exit if it leads to empty space
|
||||
if (!this.grid.occupiedCells.has(key)) {
|
||||
this.pendingExits.push({
|
||||
x: globalX,
|
||||
y: globalY,
|
||||
direction: exit.direction
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup pending exits that are now connected
|
||||
this.pendingExits = this.pendingExits.filter(ex => {
|
||||
const n = this.neighbor(ex.x, ex.y, ex.direction);
|
||||
return !this.grid.occupiedCells.has(`${n.x},${n.y}`);
|
||||
});
|
||||
}
|
||||
|
||||
neighbor(x, y, direction) {
|
||||
switch (direction) {
|
||||
case DIRECTIONS.NORTH: return { x, y: y + 1 };
|
||||
case DIRECTIONS.SOUTH: return { x, y: y - 1 };
|
||||
case DIRECTIONS.EAST: return { x: x + 1, y };
|
||||
case DIRECTIONS.WEST: return { x: x - 1, y };
|
||||
}
|
||||
}
|
||||
|
||||
opposite(direction) {
|
||||
switch (direction) {
|
||||
case DIRECTIONS.NORTH: return DIRECTIONS.SOUTH;
|
||||
case DIRECTIONS.SOUTH: return DIRECTIONS.NORTH;
|
||||
case DIRECTIONS.EAST: return DIRECTIONS.WEST;
|
||||
case DIRECTIONS.WEST: return DIRECTIONS.EAST;
|
||||
}
|
||||
}
|
||||
|
||||
shuffle(arr) {
|
||||
const copy = [...arr];
|
||||
for (let i = copy.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[copy[i], copy[j]] = [copy[j], copy[i]];
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
134
src/engine/dungeon/old/GridSystem.js
Normal file
134
src/engine/dungeon/old/GridSystem.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import { DIRECTIONS } from './Constants.js';
|
||||
|
||||
export class GridSystem {
|
||||
/**
|
||||
* The GridSystem maintains the "Source of Truth" for the dungeon layout.
|
||||
* It knows which cells are occupied and by whom.
|
||||
* Dependencies: Constants.js (DIRECTIONS)
|
||||
*/
|
||||
constructor() {
|
||||
// We use a Map for O(1) lookups.
|
||||
// Key: "x,y" (String) -> Value: "tileId" (String)
|
||||
this.occupiedCells = new Map();
|
||||
|
||||
// We also keep a list of placed tile objects for easier iteration if needed later.
|
||||
this.tiles = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a tile can be placed at the given coordinates with the given rotation.
|
||||
* Needs: The Tile Definition (to know size), the target X,Y, and desired Rotation.
|
||||
*/
|
||||
canPlace(tileDef, startX, startY, rotation) {
|
||||
// 1. Calculate the real-world coordinates of every single cell this tile would occupy.
|
||||
const cells = this.getGlobalCells(tileDef, startX, startY, rotation);
|
||||
|
||||
// 2. Check each cell against our Map of occupied spots.
|
||||
for (const cell of cells) {
|
||||
const key = `${cell.x},${cell.y}`;
|
||||
if (this.occupiedCells.has(key)) {
|
||||
return false; // COLLISION! Spot already taken.
|
||||
}
|
||||
}
|
||||
return true; // All clear.
|
||||
}
|
||||
|
||||
/**
|
||||
* Officially registers a tile onto the board.
|
||||
* Should only be called AFTER canPlace returns true.
|
||||
*/
|
||||
placeTile(tileInstance, tileDef) {
|
||||
const cells = this.getGlobalCells(tileDef, tileInstance.x, tileInstance.y, tileInstance.rotation);
|
||||
|
||||
// Record every cell in our Map
|
||||
for (const cell of cells) {
|
||||
const key = `${cell.x},${cell.y}`;
|
||||
this.occupiedCells.set(key, tileInstance.id);
|
||||
}
|
||||
|
||||
// Store the instance
|
||||
this.tiles.push(tileInstance);
|
||||
console.log(`Placed tile ${tileInstance.id} at ${tileInstance.x},${tileInstance.y}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* THE CORRECTED MAGIC MATH FUNCTION.
|
||||
* Now uses Pre-Calculated Variants from TileDefinitions.
|
||||
* NO DYNAMIC ROTATION MATH performed here.
|
||||
* All variants are treated as "North" oriented blocks.
|
||||
*/
|
||||
getGlobalCells(tileDef, startX, startY, rotation) {
|
||||
const cells = [];
|
||||
|
||||
// 1. Retrieve the specific variant for this rotation
|
||||
const variant = tileDef.variants[rotation];
|
||||
if (!variant) {
|
||||
console.error(`Missing variant for rotation ${rotation} in tile ${tileDef.id}`);
|
||||
return cells;
|
||||
}
|
||||
|
||||
const layout = variant.layout;
|
||||
|
||||
if (!layout) {
|
||||
console.error("Variant missing layout. ID:", tileDef?.id, rotation);
|
||||
return cells;
|
||||
}
|
||||
|
||||
const numberOfRows = layout.length;
|
||||
|
||||
// Iterate through matrix rows
|
||||
for (let row = 0; row < numberOfRows; row++) {
|
||||
const rowData = layout[row];
|
||||
const numberOfCols = rowData.length;
|
||||
|
||||
for (let col = 0; col < numberOfCols; col++) {
|
||||
const cellValue = rowData[col];
|
||||
|
||||
// Skip empty cells (0)
|
||||
if (cellValue === 0) continue;
|
||||
|
||||
// Map Matrix (Row, Col) to Local Grid (lx, ly)
|
||||
// Matrix Row 0 is the "Top" (Max Y) relative to the tile's origin
|
||||
// Matrix Row (Rows-1) is the "Bottom" (Y=0)
|
||||
// lx grows right (col)
|
||||
// ly grows up (rows reversed)
|
||||
const lx = col;
|
||||
const ly = (numberOfRows - 1) - row;
|
||||
|
||||
// SIMPLIFIED LOGIC:
|
||||
// Since the variant layout is ALREADY rotated, we always just ADD the offsets.
|
||||
// We treat every variant as if it's placed "North-wise" at the anchor point.
|
||||
|
||||
const gx = startX + lx;
|
||||
const gy = startY + ly;
|
||||
|
||||
cells.push({ x: gx, y: gy, value: cellValue });
|
||||
}
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a local point (like an exit definition) to Global Coordinates.
|
||||
* Simplified: Just adds offsets because variants carry pre-rotated coords.
|
||||
*/
|
||||
getGlobalPoint(localX, localY, tileInstance) {
|
||||
const startX = tileInstance.x;
|
||||
const startY = tileInstance.y;
|
||||
|
||||
// Simple translation. Rotation is handled by the variant properties upstream.
|
||||
return {
|
||||
x: startX + localX,
|
||||
y: startY + localY
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* DEPRECATED / LEGACY
|
||||
* Kept just in case, but shouldn't be needed with explicit variants.
|
||||
*/
|
||||
getRotatedDirection(originalDirection, tileRotation) {
|
||||
// With explicit variants, the exit.direction is ALREADY the final global direction.
|
||||
return originalDirection;
|
||||
}
|
||||
}
|
||||
@@ -1,196 +1,105 @@
|
||||
|
||||
import { DungeonGenerator } from '../dungeon/DungeonGenerator.js';
|
||||
import { TurnManager } from './TurnManager.js';
|
||||
import { GAME_PHASES } from './GameConstants.js';
|
||||
import { Entity } from './Entity.js';
|
||||
|
||||
/**
|
||||
* GameEngine for Manual Dungeon Construction with Player Movement
|
||||
*/
|
||||
export class GameEngine {
|
||||
constructor() {
|
||||
this.dungeon = new DungeonGenerator();
|
||||
this.turnManager = new TurnManager();
|
||||
this.missionConfig = null;
|
||||
|
||||
this.player = null;
|
||||
this.selectedEntity = null;
|
||||
this.isRunning = false;
|
||||
|
||||
this.selectedPath = [];
|
||||
this.selectedEntityId = null;
|
||||
|
||||
// Simple event callbacks (external systems can assign these)
|
||||
this.onPathChange = null; // (path) => {}
|
||||
this.onEntityUpdate = null; // (entity) => {}
|
||||
this.onEntityMove = null; // (entity, path) => {}
|
||||
this.onEntitySelect = null; // (entityId, isSelected) => {}
|
||||
|
||||
this.setupEventHooks();
|
||||
}
|
||||
|
||||
setupEventHooks() {
|
||||
this.turnManager.on('phase_changed', (phase) => this.handlePhaseChange(phase));
|
||||
// Callbacks
|
||||
this.onEntityUpdate = null;
|
||||
this.onEntityMove = null;
|
||||
this.onEntitySelect = null;
|
||||
this.onPathChange = null;
|
||||
}
|
||||
|
||||
startMission(missionConfig) {
|
||||
this.missionConfig = missionConfig;
|
||||
console.log(`[GameEngine] Starting mission: ${missionConfig.name}`);
|
||||
|
||||
// 1. Initialize Dungeon (Places the first room)
|
||||
console.log('[GameEngine] Starting mission:', missionConfig.name);
|
||||
this.dungeon.startDungeon(missionConfig);
|
||||
|
||||
// 2. Initialize Player
|
||||
// Find a valid starting spot (first occupied cell)
|
||||
const startingSpot = this.dungeon.grid.occupiedCells.keys().next().value;
|
||||
const [startX, startY] = startingSpot ? startingSpot.split(',').map(Number) : [0, 0];
|
||||
// Create player at center of first tile
|
||||
this.createPlayer(1.5, 2.5); // Center of 2x6 corridor
|
||||
|
||||
console.log(`[GameEngine] Spawning player at valid spot: ${startX}, ${startY}`);
|
||||
this.player = new Entity('p1', 'Barbaro', 'hero', startX, startY, '/assets/images/dungeon1/standees/barbaro.png');
|
||||
if (this.onEntityUpdate) this.onEntityUpdate(this.player);
|
||||
|
||||
// 3. Start the Turn Sequence
|
||||
this.turnManager.startGame();
|
||||
this.isRunning = true;
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
// Continuous logic
|
||||
createPlayer(x, y) {
|
||||
this.player = {
|
||||
id: 'p1',
|
||||
name: 'Barbarian',
|
||||
x: Math.floor(x),
|
||||
y: Math.floor(y),
|
||||
texturePath: '/assets/images/dungeon1/standees/barbaro.png'
|
||||
};
|
||||
|
||||
if (this.onEntityUpdate) {
|
||||
this.onEntityUpdate(this.player);
|
||||
}
|
||||
|
||||
console.log('[GameEngine] Player created at', this.player.x, this.player.y);
|
||||
}
|
||||
|
||||
handlePhaseChange(phase) {
|
||||
console.log(`[GameEngine] Phase Changed to: ${phase}`);
|
||||
}
|
||||
|
||||
// --- Interaction ---
|
||||
onCellClick(x, y) {
|
||||
if (this.turnManager.currentPhase !== GAME_PHASES.HERO) return;
|
||||
|
||||
console.log(`Cell Click: ${x},${y}`);
|
||||
|
||||
// 1. Selector Check (Click on Player?)
|
||||
if (this.player.x === x && this.player.y === y) {
|
||||
if (this.selectedEntityId === this.player.id) {
|
||||
// Deselect
|
||||
this.selectedEntityId = null;
|
||||
console.log("Player Deselected");
|
||||
if (this.onEntitySelect) this.onEntitySelect(this.player.id, false);
|
||||
|
||||
// Clear path too
|
||||
this.selectedPath = [];
|
||||
this._notifyPath();
|
||||
} else {
|
||||
// Select
|
||||
this.selectedEntityId = this.player.id;
|
||||
console.log("Player Selected");
|
||||
if (this.onEntitySelect) this.onEntitySelect(this.player.id, true);
|
||||
// If no player selected, select player on click
|
||||
if (!this.selectedEntity && this.player && x === this.player.x && y === this.player.y) {
|
||||
this.selectedEntity = this.player;
|
||||
if (this.onEntitySelect) {
|
||||
this.onEntitySelect(this.player.id, true);
|
||||
}
|
||||
console.log('[GameEngine] Player selected');
|
||||
return;
|
||||
}
|
||||
|
||||
// If nothing selected, ignore floor clicks
|
||||
if (!this.selectedEntityId) return;
|
||||
|
||||
// 2. Check if valid Floor (Occupied Cell)
|
||||
if (!this.dungeon.grid.occupiedCells.has(`${x},${y}`)) {
|
||||
console.log("Invalid cell: Void");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Logic: Path Building (Only if Selected)
|
||||
|
||||
// A. If clicking on last selected -> Deselect (Remove last step)
|
||||
if (this.selectedPath.length > 0) {
|
||||
const last = this.selectedPath[this.selectedPath.length - 1];
|
||||
if (last.x === x && last.y === y) {
|
||||
this.selectedPath.pop();
|
||||
this._notifyPath();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// B. Determine Previous Point (Player or Last Path Node)
|
||||
let prevX = this.player.x;
|
||||
let prevY = this.player.y;
|
||||
|
||||
if (this.selectedPath.length > 0) {
|
||||
const last = this.selectedPath[this.selectedPath.length - 1];
|
||||
prevX = last.x;
|
||||
prevY = last.y;
|
||||
}
|
||||
|
||||
// Note: Manhattan distance 1 = Adjacency (No diagonals)
|
||||
const dist = Math.abs(x - prevX) + Math.abs(y - prevY);
|
||||
|
||||
if (dist === 1) {
|
||||
// Check Path Length Limit (Speed)
|
||||
if (this.selectedPath.length < this.player.stats.move) {
|
||||
this.selectedPath.push({ x, y });
|
||||
this._notifyPath();
|
||||
// If player selected, move to clicked cell
|
||||
if (this.selectedEntity === this.player) {
|
||||
if (this.canMoveTo(x, y)) {
|
||||
this.movePlayer(x, y);
|
||||
} else {
|
||||
console.log("Max movement reached");
|
||||
}
|
||||
} else {
|
||||
// Restart path if clicking adjacent to player
|
||||
const distFromPlayer = Math.abs(x - this.player.x) + Math.abs(y - this.player.y);
|
||||
if (distFromPlayer === 1) {
|
||||
this.selectedPath = [{ x, y }];
|
||||
this._notifyPath();
|
||||
console.log('[GameEngine] Cannot move there');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onCellRightClick(x, y) {
|
||||
if (this.turnManager.currentPhase !== GAME_PHASES.HERO) return;
|
||||
if (!this.selectedEntityId) return; // Must satisfy selection rule
|
||||
canMoveTo(x, y) {
|
||||
// Check if cell is walkable (occupied by a tile)
|
||||
return this.dungeon.grid.isOccupied(x, y);
|
||||
}
|
||||
|
||||
// Must be clicking the last tile of the path to confirm
|
||||
if (this.selectedPath.length === 0) return;
|
||||
movePlayer(x, y) {
|
||||
// Simple direct movement (no pathfinding for now)
|
||||
const path = [{ x, y }];
|
||||
|
||||
const last = this.selectedPath[this.selectedPath.length - 1];
|
||||
if (last.x === x && last.y === y) {
|
||||
console.log("Confirming Move...");
|
||||
this.confirmMove();
|
||||
this.player.x = x;
|
||||
this.player.y = y;
|
||||
|
||||
if (this.onEntityMove) {
|
||||
this.onEntityMove(this.player, path);
|
||||
}
|
||||
}
|
||||
|
||||
confirmMove() {
|
||||
if (this.selectedPath.length === 0) return;
|
||||
|
||||
const target = this.selectedPath[this.selectedPath.length - 1];
|
||||
const pathCopy = [...this.selectedPath];
|
||||
|
||||
// 1. Trigger Animation Sequence
|
||||
if (this.onEntityMove) this.onEntityMove(this.player, pathCopy);
|
||||
|
||||
// 2. Update Logical Position
|
||||
this.player.setPosition(target.x, target.y);
|
||||
|
||||
// 3. Cleanup
|
||||
this.selectedPath = [];
|
||||
this._notifyPath();
|
||||
|
||||
// Note: Exploration is now manual via door interaction
|
||||
}
|
||||
|
||||
_notifyPath() {
|
||||
if (this.onPathChange) this.onPathChange(this.selectedPath);
|
||||
}
|
||||
|
||||
exploreExit(exitCell) {
|
||||
console.log('[GameEngine] Exploring exit:', exitCell);
|
||||
|
||||
// Find this exit in pendingExits
|
||||
const exit = this.dungeon.pendingExits.find(ex => ex.x === exitCell.x && ex.y === exitCell.y);
|
||||
|
||||
if (exit) {
|
||||
// Prioritize this exit
|
||||
const idx = this.dungeon.pendingExits.indexOf(exit);
|
||||
if (idx > -1) {
|
||||
this.dungeon.pendingExits.splice(idx, 1);
|
||||
this.dungeon.pendingExits.unshift(exit);
|
||||
}
|
||||
|
||||
// Trigger exploration
|
||||
this.turnManager.triggerExploration();
|
||||
this.dungeon.step();
|
||||
this.turnManager.setPhase(GAME_PHASES.HERO);
|
||||
} else {
|
||||
console.warn('[GameEngine] Exit not found in pendingExits');
|
||||
// Deselect after move
|
||||
this.selectedEntity = null;
|
||||
if (this.onEntitySelect) {
|
||||
this.onEntitySelect(this.player.id, false);
|
||||
}
|
||||
|
||||
console.log('[GameEngine] Player moved to', x, y);
|
||||
}
|
||||
|
||||
isPlayerAdjacentToDoor(doorExit) {
|
||||
if (!this.player) return false;
|
||||
|
||||
const dx = Math.abs(this.player.x - doorExit.x);
|
||||
const dy = Math.abs(this.player.y - doorExit.y);
|
||||
|
||||
// Adjacent means distance of 1 in one direction and 0 in the other
|
||||
return (dx === 1 && dy === 0) || (dx === 0 && dy === 1);
|
||||
}
|
||||
|
||||
update(time) {
|
||||
// Minimal update loop
|
||||
}
|
||||
}
|
||||
|
||||
196
src/engine/game/old/GameEngine_20260102.js
Normal file
196
src/engine/game/old/GameEngine_20260102.js
Normal file
@@ -0,0 +1,196 @@
|
||||
|
||||
import { DungeonGenerator } from '../dungeon/DungeonGenerator.js';
|
||||
import { TurnManager } from './TurnManager.js';
|
||||
import { GAME_PHASES } from './GameConstants.js';
|
||||
import { Entity } from './Entity.js';
|
||||
|
||||
export class GameEngine {
|
||||
constructor() {
|
||||
this.dungeon = new DungeonGenerator();
|
||||
this.turnManager = new TurnManager();
|
||||
this.missionConfig = null;
|
||||
|
||||
this.player = null;
|
||||
|
||||
this.selectedPath = [];
|
||||
this.selectedEntityId = null;
|
||||
|
||||
// Simple event callbacks (external systems can assign these)
|
||||
this.onPathChange = null; // (path) => {}
|
||||
this.onEntityUpdate = null; // (entity) => {}
|
||||
this.onEntityMove = null; // (entity, path) => {}
|
||||
this.onEntitySelect = null; // (entityId, isSelected) => {}
|
||||
|
||||
this.setupEventHooks();
|
||||
}
|
||||
|
||||
setupEventHooks() {
|
||||
this.turnManager.on('phase_changed', (phase) => this.handlePhaseChange(phase));
|
||||
}
|
||||
|
||||
startMission(missionConfig) {
|
||||
this.missionConfig = missionConfig;
|
||||
console.log(`[GameEngine] Starting mission: ${missionConfig.name}`);
|
||||
|
||||
// 1. Initialize Dungeon (Places the first room)
|
||||
this.dungeon.startDungeon(missionConfig);
|
||||
|
||||
// 2. Initialize Player
|
||||
// Find a valid starting spot (first occupied cell)
|
||||
const startingSpot = this.dungeon.grid.occupiedCells.keys().next().value;
|
||||
const [startX, startY] = startingSpot ? startingSpot.split(',').map(Number) : [0, 0];
|
||||
|
||||
console.log(`[GameEngine] Spawning player at valid spot: ${startX}, ${startY}`);
|
||||
this.player = new Entity('p1', 'Barbaro', 'hero', startX, startY, '/assets/images/dungeon1/standees/barbaro.png');
|
||||
if (this.onEntityUpdate) this.onEntityUpdate(this.player);
|
||||
|
||||
// 3. Start the Turn Sequence
|
||||
this.turnManager.startGame();
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
// Continuous logic
|
||||
}
|
||||
|
||||
handlePhaseChange(phase) {
|
||||
console.log(`[GameEngine] Phase Changed to: ${phase}`);
|
||||
}
|
||||
|
||||
// --- Interaction ---
|
||||
onCellClick(x, y) {
|
||||
if (this.turnManager.currentPhase !== GAME_PHASES.HERO) return;
|
||||
|
||||
console.log(`Cell Click: ${x},${y}`);
|
||||
|
||||
// 1. Selector Check (Click on Player?)
|
||||
if (this.player.x === x && this.player.y === y) {
|
||||
if (this.selectedEntityId === this.player.id) {
|
||||
// Deselect
|
||||
this.selectedEntityId = null;
|
||||
console.log("Player Deselected");
|
||||
if (this.onEntitySelect) this.onEntitySelect(this.player.id, false);
|
||||
|
||||
// Clear path too
|
||||
this.selectedPath = [];
|
||||
this._notifyPath();
|
||||
} else {
|
||||
// Select
|
||||
this.selectedEntityId = this.player.id;
|
||||
console.log("Player Selected");
|
||||
if (this.onEntitySelect) this.onEntitySelect(this.player.id, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If nothing selected, ignore floor clicks
|
||||
if (!this.selectedEntityId) return;
|
||||
|
||||
// 2. Check if valid Floor (Occupied Cell)
|
||||
if (!this.dungeon.grid.occupiedCells.has(`${x},${y}`)) {
|
||||
console.log("Invalid cell: Void");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Logic: Path Building (Only if Selected)
|
||||
|
||||
// A. If clicking on last selected -> Deselect (Remove last step)
|
||||
if (this.selectedPath.length > 0) {
|
||||
const last = this.selectedPath[this.selectedPath.length - 1];
|
||||
if (last.x === x && last.y === y) {
|
||||
this.selectedPath.pop();
|
||||
this._notifyPath();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// B. Determine Previous Point (Player or Last Path Node)
|
||||
let prevX = this.player.x;
|
||||
let prevY = this.player.y;
|
||||
|
||||
if (this.selectedPath.length > 0) {
|
||||
const last = this.selectedPath[this.selectedPath.length - 1];
|
||||
prevX = last.x;
|
||||
prevY = last.y;
|
||||
}
|
||||
|
||||
// Note: Manhattan distance 1 = Adjacency (No diagonals)
|
||||
const dist = Math.abs(x - prevX) + Math.abs(y - prevY);
|
||||
|
||||
if (dist === 1) {
|
||||
// Check Path Length Limit (Speed)
|
||||
if (this.selectedPath.length < this.player.stats.move) {
|
||||
this.selectedPath.push({ x, y });
|
||||
this._notifyPath();
|
||||
} else {
|
||||
console.log("Max movement reached");
|
||||
}
|
||||
} else {
|
||||
// Restart path if clicking adjacent to player
|
||||
const distFromPlayer = Math.abs(x - this.player.x) + Math.abs(y - this.player.y);
|
||||
if (distFromPlayer === 1) {
|
||||
this.selectedPath = [{ x, y }];
|
||||
this._notifyPath();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onCellRightClick(x, y) {
|
||||
if (this.turnManager.currentPhase !== GAME_PHASES.HERO) return;
|
||||
if (!this.selectedEntityId) return; // Must satisfy selection rule
|
||||
|
||||
// Must be clicking the last tile of the path to confirm
|
||||
if (this.selectedPath.length === 0) return;
|
||||
|
||||
const last = this.selectedPath[this.selectedPath.length - 1];
|
||||
if (last.x === x && last.y === y) {
|
||||
console.log("Confirming Move...");
|
||||
this.confirmMove();
|
||||
}
|
||||
}
|
||||
|
||||
confirmMove() {
|
||||
if (this.selectedPath.length === 0) return;
|
||||
|
||||
const target = this.selectedPath[this.selectedPath.length - 1];
|
||||
const pathCopy = [...this.selectedPath];
|
||||
|
||||
// 1. Trigger Animation Sequence
|
||||
if (this.onEntityMove) this.onEntityMove(this.player, pathCopy);
|
||||
|
||||
// 2. Update Logical Position
|
||||
this.player.setPosition(target.x, target.y);
|
||||
|
||||
// 3. Cleanup
|
||||
this.selectedPath = [];
|
||||
this._notifyPath();
|
||||
|
||||
// Note: Exploration is now manual via door interaction
|
||||
}
|
||||
|
||||
_notifyPath() {
|
||||
if (this.onPathChange) this.onPathChange(this.selectedPath);
|
||||
}
|
||||
|
||||
exploreExit(exitCell) {
|
||||
console.log('[GameEngine] Exploring exit:', exitCell);
|
||||
|
||||
// Find this exit in pendingExits
|
||||
const exit = this.dungeon.pendingExits.find(ex => ex.x === exitCell.x && ex.y === exitCell.y);
|
||||
|
||||
if (exit) {
|
||||
// Prioritize this exit
|
||||
const idx = this.dungeon.pendingExits.indexOf(exit);
|
||||
if (idx > -1) {
|
||||
this.dungeon.pendingExits.splice(idx, 1);
|
||||
this.dungeon.pendingExits.unshift(exit);
|
||||
}
|
||||
|
||||
// Trigger exploration
|
||||
this.turnManager.triggerExploration();
|
||||
this.dungeon.step();
|
||||
this.turnManager.setPhase(GAME_PHASES.HERO);
|
||||
} else {
|
||||
console.warn('[GameEngine] Exit not found in pendingExits');
|
||||
}
|
||||
}
|
||||
}
|
||||
154
src/main.js
154
src/main.js
@@ -1,69 +1,50 @@
|
||||
|
||||
import { GameEngine } from './engine/game/GameEngine.js';
|
||||
import { GameRenderer } from './view/GameRenderer.js';
|
||||
import { CameraManager } from './view/CameraManager.js';
|
||||
import { UIManager } from './view/UIManager.js';
|
||||
import { DoorModal } from './view/DoorModal.js';
|
||||
import { MissionConfig, MISSION_TYPES } from './engine/dungeon/MissionConfig.js';
|
||||
|
||||
console.log("Initializing Warhammer Quest Engine... SYSTEM: GAME_LOOP_ARC_V1");
|
||||
window.TEXTURE_DEBUG = true;
|
||||
console.log("🏗️ Warhammer Quest - Manual Dungeon Construction");
|
||||
|
||||
// 1. Setup Mission
|
||||
const mission = new MissionConfig({
|
||||
id: 'mission_1',
|
||||
name: 'The First Dive',
|
||||
name: 'Manual Construction',
|
||||
type: MISSION_TYPES.ESCAPE,
|
||||
minTiles: 6
|
||||
minTiles: 13
|
||||
});
|
||||
|
||||
// 2. Initialize Core Systems
|
||||
const renderer = new GameRenderer('app'); // Visuals
|
||||
const cameraManager = new CameraManager(renderer); // Camera
|
||||
const game = new GameEngine(); // Logic Brain
|
||||
|
||||
// 3. Initialize UI
|
||||
// UIManager currently reads directly from DungeonGenerator for minimap
|
||||
const renderer = new GameRenderer('app');
|
||||
const cameraManager = new CameraManager(renderer);
|
||||
const game = new GameEngine();
|
||||
const ui = new UIManager(cameraManager, game);
|
||||
const doorModal = new DoorModal();
|
||||
|
||||
// Global Access for Debugging in Browser Console
|
||||
// Global Access
|
||||
window.GAME = game;
|
||||
window.RENDERER = renderer;
|
||||
|
||||
// 4. Bridge Logic & View (Event Hook)
|
||||
// When logic places a tile, we tell the renderer to spawn 3D meshes.
|
||||
// Ideally, this should be an Event in GameEngine, but we keep this patch for now to verify.
|
||||
// 3. Connect Dungeon Generator to Renderer
|
||||
const generator = game.dungeon;
|
||||
const originalPlaceTile = generator.grid.placeTile.bind(generator.grid);
|
||||
|
||||
generator.grid.placeTile = (instance, def) => {
|
||||
// 1. Execute Logic
|
||||
originalPlaceTile(instance, def);
|
||||
generator.grid.placeTile = (instance, variant, card) => {
|
||||
originalPlaceTile(instance, variant);
|
||||
|
||||
// 2. Execute Visuals
|
||||
const cells = generator.grid.getGlobalCells(def, instance.x, instance.y, instance.rotation);
|
||||
renderer.addTile(cells, def.type, def, instance);
|
||||
const cells = generator.grid.calculateCells(variant, instance.x, instance.y);
|
||||
renderer.addTile(cells, card.type, card, instance);
|
||||
|
||||
// 3. Update Exits Visuals
|
||||
setTimeout(() => {
|
||||
renderer.renderExits(generator.pendingExits);
|
||||
}, 50); // Small delay to ensure logic updated pendingExits
|
||||
renderer.renderExits(generator.availableExits);
|
||||
}, 50);
|
||||
};
|
||||
|
||||
// 5. Connect UI Buttons to Game Actions (Temporary)
|
||||
// We will add a temporary button in pure JS here or modify UIManager later.
|
||||
// For now, let's expose a global function for the UI to call if needed,
|
||||
// or simply rely on UIManager updates.
|
||||
|
||||
// 6. Start the Game
|
||||
// 5a. Bridge Game Interactions
|
||||
// 5a. Bridge Game Interactions
|
||||
// 4. Connect Player to Renderer
|
||||
game.onEntityUpdate = (entity) => {
|
||||
renderer.addEntity(entity);
|
||||
renderer.updateEntityPosition(entity);
|
||||
|
||||
// Initial Center on Player Spawn
|
||||
// Center camera on player spawn
|
||||
if (entity.id === 'p1' && !entity._centered) {
|
||||
cameraManager.centerOn(entity.x, entity.y);
|
||||
entity._centered = true;
|
||||
@@ -79,85 +60,84 @@ game.onEntitySelect = (entityId, isSelected) => {
|
||||
};
|
||||
|
||||
renderer.onHeroFinishedMove = (x, y) => {
|
||||
// x, y are World Coordinates (x, -z grid)
|
||||
// Actually, renderer returns Mesh Position.
|
||||
// Mesh X = Grid X. Mesh Z = -Grid Y.
|
||||
// Camera centerOn takes (Grid X, Grid Y).
|
||||
// So we need to convert back?
|
||||
// centerOn implementation: this.target.set(x, 0, -y);
|
||||
// If onHeroFinishedMove passes (mesh.x, -mesh.z), that is (Grid X, Grid Y).
|
||||
|
||||
// Let's verify what we passed in renderer:
|
||||
// this.onHeroFinishedMove(mesh.position.x, -mesh.position.z);
|
||||
// So if mesh is at (5, 1.5, -5), we pass (5, 5).
|
||||
// centerOn(5, 5) -> target(5, 0, -5). Correct.
|
||||
|
||||
cameraManager.centerOn(x, y);
|
||||
};
|
||||
|
||||
game.onPathChange = (path) => {
|
||||
renderer.highlightCells(path);
|
||||
// 5. Connect Generator State to UI
|
||||
generator.onStateChange = (state) => {
|
||||
console.log(`[Main] State: ${state}`);
|
||||
|
||||
if (state === 'PLACING_TILE') {
|
||||
ui.showPlacementControls(true);
|
||||
} else {
|
||||
ui.showPlacementControls(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Custom click handler that checks for doors first
|
||||
const handleCellClick = async (x, y, doorMesh) => {
|
||||
// If doorMesh is provided, user clicked directly on a door texture
|
||||
generator.onPlacementUpdate = (preview) => {
|
||||
if (preview) {
|
||||
renderer.showPlacementPreview(preview);
|
||||
ui.updatePlacementStatus(preview.isValid);
|
||||
} else {
|
||||
renderer.hidePlacementPreview();
|
||||
}
|
||||
};
|
||||
|
||||
// 6. Handle Clicks
|
||||
const handleClick = (x, y, doorMesh) => {
|
||||
// PRIORITY 1: Tile Placement Mode - ignore all clicks
|
||||
if (generator.state === 'PLACING_TILE') {
|
||||
console.log('[Main] Use placement controls to place tile');
|
||||
return;
|
||||
}
|
||||
|
||||
// PRIORITY 2: Door Click (must be adjacent to player)
|
||||
if (doorMesh && doorMesh.userData.isDoor && !doorMesh.userData.isOpen) {
|
||||
// Get player position
|
||||
const player = game.player;
|
||||
if (!player) {
|
||||
console.log('[Main] Player not found');
|
||||
return;
|
||||
}
|
||||
const doorExit = doorMesh.userData.cells[0];
|
||||
|
||||
// Check if player is adjacent to the door
|
||||
if (renderer.isPlayerAdjacentToDoor(player.x, player.y, doorMesh)) {
|
||||
// Show modal
|
||||
const confirmed = await doorModal.show('¿Quieres abrir la puerta?');
|
||||
if (game.isPlayerAdjacentToDoor(doorExit)) {
|
||||
console.log('[Main] 🚪 Opening door and drawing tile...');
|
||||
|
||||
if (confirmed) {
|
||||
// Open the door
|
||||
renderer.openDoor(doorMesh);
|
||||
// Open door visually
|
||||
renderer.openDoor(doorMesh);
|
||||
|
||||
// Trigger exploration of the next tile
|
||||
const exitCell = doorMesh.userData.cells[0];
|
||||
console.log('[Main] Opening door at exit:', exitCell);
|
||||
|
||||
// Call game logic to explore through this exit
|
||||
game.exploreExit(exitCell);
|
||||
// Get proper exit data with direction
|
||||
const exitData = doorMesh.userData.exitData;
|
||||
if (exitData) {
|
||||
generator.selectDoor(exitData);
|
||||
} else {
|
||||
console.error('[Main] Door missing exitData');
|
||||
}
|
||||
} else {
|
||||
console.log('[Main] Player is not adjacent to the door. Move closer first.');
|
||||
console.log('[Main] ⚠️ Move adjacent to the door first');
|
||||
}
|
||||
} else if (x !== null && y !== null) {
|
||||
// Normal cell click (no door involved)
|
||||
return;
|
||||
}
|
||||
|
||||
// PRIORITY 3: Normal cell click (player selection/movement)
|
||||
if (x !== null && y !== null) {
|
||||
game.onCellClick(x, y);
|
||||
}
|
||||
};
|
||||
|
||||
renderer.setupInteraction(
|
||||
() => cameraManager.getCamera(),
|
||||
handleCellClick,
|
||||
(x, y) => game.onCellRightClick(x, y)
|
||||
handleClick,
|
||||
() => { } // No right-click
|
||||
);
|
||||
|
||||
console.log("--- Starting Game Session ---");
|
||||
// 7. Start
|
||||
console.log("--- Starting Game ---");
|
||||
game.startMission(mission);
|
||||
|
||||
// 7. Render Loop
|
||||
// 8. Render Loop
|
||||
const animate = (time) => {
|
||||
requestAnimationFrame(animate);
|
||||
|
||||
// Update Game Logic (State Machine, Timers, etc)
|
||||
game.update(time);
|
||||
|
||||
// Update Camera Animations
|
||||
cameraManager.update(time);
|
||||
|
||||
// Update Visual Animations
|
||||
renderer.updateAnimations(time);
|
||||
|
||||
// Render Frame
|
||||
renderer.render(cameraManager.getCamera());
|
||||
};
|
||||
animate(0);
|
||||
|
||||
console.log("✅ Ready - Move barbarian next to a door and click it");
|
||||
|
||||
172
src/old/main_20260102.js
Normal file
172
src/old/main_20260102.js
Normal file
@@ -0,0 +1,172 @@
|
||||
|
||||
import { GameEngine } from './engine/game/GameEngine.js';
|
||||
import { GameRenderer } from './view/GameRenderer.js';
|
||||
import { CameraManager } from './view/CameraManager.js';
|
||||
import { UIManager } from './view/UIManager.js';
|
||||
import { DoorModal } from './view/DoorModal.js';
|
||||
import { MissionConfig, MISSION_TYPES } from './engine/dungeon/MissionConfig.js';
|
||||
|
||||
console.log("Initializing Warhammer Quest Engine... SYSTEM: MANUAL_PLACEMENT_V2");
|
||||
window.TEXTURE_DEBUG = true;
|
||||
|
||||
// 1. Setup Mission
|
||||
const mission = new MissionConfig({
|
||||
id: 'mission_1',
|
||||
name: 'The First Dive',
|
||||
type: MISSION_TYPES.ESCAPE,
|
||||
minTiles: 6
|
||||
});
|
||||
|
||||
// 2. Initialize Core Systems
|
||||
const renderer = new GameRenderer('app');
|
||||
const cameraManager = new CameraManager(renderer);
|
||||
const game = new GameEngine();
|
||||
|
||||
// 3. Initialize UI
|
||||
const ui = new UIManager(cameraManager, game);
|
||||
const doorModal = new DoorModal();
|
||||
|
||||
// Global Access for Debugging
|
||||
window.GAME = game;
|
||||
window.RENDERER = renderer;
|
||||
|
||||
// 4. Bridge Logic & View
|
||||
const generator = game.dungeon;
|
||||
const originalPlaceTile = generator.grid.placeTile.bind(generator.grid);
|
||||
|
||||
// Override placeTile to trigger rendering
|
||||
generator.grid.placeTile = (instance, variant, def) => {
|
||||
originalPlaceTile(instance, variant);
|
||||
|
||||
const cells = generator.grid.calculateCells(variant, instance.x, instance.y);
|
||||
renderer.addTile(cells, def.type, def, instance);
|
||||
|
||||
// Update exits visualization
|
||||
setTimeout(() => {
|
||||
renderer.renderExits(generator.availableExits);
|
||||
}, 50);
|
||||
};
|
||||
|
||||
// 5. Connect Generator State Changes to UI
|
||||
generator.onStateChange = (state) => {
|
||||
console.log(`[Main] Generator state: ${state}`);
|
||||
|
||||
if (state === 'PLACING_TILE') {
|
||||
ui.showPlacementControls(true);
|
||||
} else {
|
||||
ui.showPlacementControls(false);
|
||||
}
|
||||
|
||||
if (state === 'WAITING_DOOR') {
|
||||
// Enable door selection mode
|
||||
renderer.enableDoorSelection(true);
|
||||
} else {
|
||||
renderer.enableDoorSelection(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 6. Connect Placement Updates to Renderer
|
||||
generator.onPlacementUpdate = (preview) => {
|
||||
if (preview) {
|
||||
renderer.showPlacementPreview(preview);
|
||||
ui.updatePlacementStatus(preview.isValid);
|
||||
} else {
|
||||
renderer.hidePlacementPreview();
|
||||
}
|
||||
};
|
||||
|
||||
// 7. Handle Door Selection
|
||||
renderer.onDoorSelected = (exitPoint) => {
|
||||
console.log('[Main] Door selected:', exitPoint);
|
||||
generator.selectDoor(exitPoint);
|
||||
};
|
||||
|
||||
// 8. Bridge Game Interactions (Player movement, etc.)
|
||||
game.onEntityUpdate = (entity) => {
|
||||
renderer.addEntity(entity);
|
||||
renderer.updateEntityPosition(entity);
|
||||
|
||||
if (entity.id === 'p1' && !entity._centered) {
|
||||
cameraManager.centerOn(entity.x, entity.y);
|
||||
entity._centered = true;
|
||||
}
|
||||
};
|
||||
|
||||
game.onEntityMove = (entity, path) => {
|
||||
renderer.moveEntityAlongPath(entity, path);
|
||||
};
|
||||
|
||||
game.onEntitySelect = (entityId, isSelected) => {
|
||||
renderer.toggleEntitySelection(entityId, isSelected);
|
||||
};
|
||||
|
||||
renderer.onHeroFinishedMove = (x, y) => {
|
||||
cameraManager.centerOn(x, y);
|
||||
};
|
||||
|
||||
game.onPathChange = (path) => {
|
||||
renderer.highlightCells(path);
|
||||
};
|
||||
|
||||
// 9. Custom click handler
|
||||
const handleCellClick = async (x, y, doorMesh) => {
|
||||
// PRIORITY 1: Check if we're in door selection mode (dungeon building)
|
||||
if (generator.state === 'WAITING_DOOR') {
|
||||
if (doorMesh && doorMesh.userData.isExit) {
|
||||
// This is an exit door that can be selected for expansion
|
||||
const exitData = doorMesh.userData.exitData;
|
||||
if (exitData) {
|
||||
console.log('[Main] Door selected for expansion:', exitData);
|
||||
generator.selectDoor(exitData);
|
||||
}
|
||||
return; // Don't process any other click logic
|
||||
}
|
||||
// If not clicking on a door in WAITING_DOOR mode, ignore the click
|
||||
console.log('[Main] Click ignored - waiting for door selection');
|
||||
return;
|
||||
}
|
||||
|
||||
// PRIORITY 2: Normal door interaction (during gameplay with player)
|
||||
if (doorMesh && doorMesh.userData.isDoor && !doorMesh.userData.isOpen) {
|
||||
const player = game.player;
|
||||
if (!player) {
|
||||
console.log('[Main] Player not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (renderer.isPlayerAdjacentToDoor(player.x, player.y, doorMesh)) {
|
||||
const confirmed = await doorModal.show('¿Quieres abrir la puerta?');
|
||||
|
||||
if (confirmed) {
|
||||
renderer.openDoor(doorMesh);
|
||||
const exitCell = doorMesh.userData.cells[0];
|
||||
console.log('[Main] Opening door at exit:', exitCell);
|
||||
game.exploreExit(exitCell);
|
||||
}
|
||||
} else {
|
||||
console.log('[Main] Player is not adjacent to the door. Move closer first.');
|
||||
}
|
||||
} else if (x !== null && y !== null) {
|
||||
game.onCellClick(x, y);
|
||||
}
|
||||
};
|
||||
|
||||
renderer.setupInteraction(
|
||||
() => cameraManager.getCamera(),
|
||||
handleCellClick,
|
||||
(x, y) => game.onCellRightClick(x, y)
|
||||
);
|
||||
|
||||
console.log("--- Starting Game Session ---");
|
||||
game.startMission(mission);
|
||||
|
||||
// 10. Render Loop
|
||||
const animate = (time) => {
|
||||
requestAnimationFrame(animate);
|
||||
|
||||
game.update(time);
|
||||
cameraManager.update(time);
|
||||
renderer.updateAnimations(time);
|
||||
renderer.render(cameraManager.getCamera());
|
||||
};
|
||||
animate(0);
|
||||
@@ -412,11 +412,18 @@ export class GameRenderer {
|
||||
mesh.rotation.y = angle;
|
||||
|
||||
// Store door data for interaction (new doors always start closed)
|
||||
// Convert numeric direction to string for generator compatibility
|
||||
const dirMap = { 0: 'N', 1: 'E', 2: 'S', 3: 'W' };
|
||||
mesh.userData = {
|
||||
isDoor: true,
|
||||
isOpen: false,
|
||||
cells: [d1, d2],
|
||||
direction: dir
|
||||
direction: dir,
|
||||
exitData: {
|
||||
x: d1.x,
|
||||
y: d1.y,
|
||||
direction: dirMap[dir] || 'N'
|
||||
}
|
||||
};
|
||||
mesh.name = `door_${idx}`;
|
||||
|
||||
@@ -480,14 +487,38 @@ export class GameRenderer {
|
||||
|
||||
// Load texture with callback
|
||||
this.getTexture(texturePath, (texture) => {
|
||||
const w = tileDef.width;
|
||||
const l = tileDef.length;
|
||||
|
||||
// Create Plane
|
||||
const geometry = new THREE.PlaneGeometry(w, l);
|
||||
// --- NEW LOGIC: Calculate center based on DIMENSIONS, not CELLS ---
|
||||
|
||||
// 1. Get the specific variant for this rotation to know the VISUAL bounds
|
||||
// (The shape the grid sees: e.g. 4x2 for East)
|
||||
const currentVariant = tileDef.variants[tileInstance.rotation];
|
||||
|
||||
if (!currentVariant) {
|
||||
console.error(`[GameRenderer] Missing variant for rotation ${tileInstance.rotation}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const rotWidth = currentVariant.width;
|
||||
const rotHeight = currentVariant.height;
|
||||
|
||||
// 2. Calculate the Geometric Center of the tile relative to the anchor
|
||||
// Formula: anchor + (dimension - 1) / 2
|
||||
// (Subtract 1 because width 1 is just offset 0)
|
||||
const cx = tileInstance.x + (rotWidth - 1) / 2;
|
||||
const cy = tileInstance.y + (rotHeight - 1) / 2;
|
||||
|
||||
console.log(`[GameRenderer] Dimensions (Rotated): ${rotWidth}x${rotHeight}`);
|
||||
console.log(`[GameRenderer] Calculated Center: (${cx}, ${cy})`);
|
||||
|
||||
// 3. Use BASE dimensions from NORTH variant for the Plane
|
||||
// (Since we are rotating the plane itself, we start with the un-rotated image size)
|
||||
const baseWidth = tileDef.variants.N.width;
|
||||
const baseHeight = tileDef.variants.N.height;
|
||||
|
||||
// Create Plane with BASE dimensions
|
||||
const geometry = new THREE.PlaneGeometry(baseWidth, baseHeight);
|
||||
|
||||
// SWITCH TO BASIC MATERIAL FOR DEBUGGING TEXTURE VISIBILITY
|
||||
// Standard material heavily depends on lights. If light is not hitting correctly, it looks black.
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
@@ -496,42 +527,32 @@ export class GameRenderer {
|
||||
});
|
||||
const plane = new THREE.Mesh(geometry, material);
|
||||
|
||||
|
||||
|
||||
// Initial Rotation: Plane X-Y to X-Z
|
||||
// Initial Rotation: Plane X-Y to X-Z (Flat on ground)
|
||||
plane.rotation.x = -Math.PI / 2;
|
||||
|
||||
// Handle Rotation safely (Support both 0-3 and N-W)
|
||||
const rotMap = { 'N': 0, '0': 0, 0: 0, 'E': 1, '1': 1, 1: 1, 'S': 2, '2': 2, 2: 2, 'W': 3, '3': 3, 3: 3 };
|
||||
// Handle Rotation
|
||||
const rotMap = { 'N': 0, 'E': 1, 'S': 2, 'W': 3 };
|
||||
const r = rotMap[tileInstance.rotation] !== undefined ? rotMap[tileInstance.rotation] : 0;
|
||||
|
||||
// Apply Tile Rotation
|
||||
// Apply Tile Rotation (Z-axis is Up in this local frame before X-rotation? No, after X-rot)
|
||||
// Actually, standard hierarchy: Rotate Z first?
|
||||
// ThreeJS rotation order XYZ.
|
||||
// We want to rotate around the Y axis of the world (which is Z of the plane before x-rotation?)
|
||||
// Simplest: Rotate Z of the plane, which corresponds to world Y.
|
||||
// Note: We use negative rotation because ThreeJS is CCW, but our grid might be different,
|
||||
// but usually -r * PI/2 works for this setup.
|
||||
plane.rotation.z = -r * (Math.PI / 2);
|
||||
|
||||
// Calculate Center Offset for Positioning
|
||||
const midX = (tileDef.width - 1) / 2;
|
||||
const midY = (tileDef.length - 1) / 2;
|
||||
|
||||
// Rotate the offset vector based on tile rotation
|
||||
let dx, dy;
|
||||
|
||||
if (r === 0) { dx = midX; dy = midY; }
|
||||
else if (r === 1) { dx = midY; dy = -midX; }
|
||||
else if (r === 2) { dx = -midX; dy = -midY; }
|
||||
else if (r === 3) { dx = -midY; dy = midX; }
|
||||
|
||||
const centerX = tileInstance.x + dx;
|
||||
const centerY = tileInstance.y + dy;
|
||||
|
||||
// Set at almost 0 height to avoid z-fighting with grid helper, but effectively on floor
|
||||
plane.position.set(centerX, 0.01, -centerY);
|
||||
// Position at the calculated center
|
||||
// Notice: World Z is -Grid Y
|
||||
plane.position.set(cx, 0.01, -cy);
|
||||
plane.receiveShadow = true;
|
||||
|
||||
this.scene.add(plane);
|
||||
console.log(`[GameRenderer] ✓ Tile plane added at (${centerX}, 0.01, ${-centerY}) for ${tileDef.id}`);
|
||||
console.log(`[GameRenderer] ✓ Tile plane added at (${cx}, 0.01, ${-cy})`);
|
||||
});
|
||||
} else {
|
||||
console.warn(`[GameRenderer] details missing for texture render. def: ${!!tileDef}, inst: ${!!tileInstance}, tex: ${tileDef?.textures?.length}`);
|
||||
console.warn(`[GameRenderer] details missing for texture render. def: ${!!tileDef}, inst: ${!!tileInstance}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,4 +602,156 @@ export class GameRenderer {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ========== MANUAL PLACEMENT SYSTEM ==========
|
||||
|
||||
enableDoorSelection(enabled) {
|
||||
this.doorSelectionEnabled = enabled;
|
||||
|
||||
if (enabled) {
|
||||
// Highlight available exits
|
||||
this.highlightAvailableExits();
|
||||
} else {
|
||||
// Remove highlights
|
||||
if (this.exitHighlightGroup) {
|
||||
this.exitHighlightGroup.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
highlightAvailableExits() {
|
||||
if (!this.exitHighlightGroup) {
|
||||
this.exitHighlightGroup = new THREE.Group();
|
||||
this.scene.add(this.exitHighlightGroup);
|
||||
}
|
||||
|
||||
this.exitHighlightGroup.clear();
|
||||
|
||||
// Highlight each exit door with a pulsing glow
|
||||
if (this.exitGroup) {
|
||||
this.exitGroup.children.forEach(doorMesh => {
|
||||
if (doorMesh.userData.isDoor && !doorMesh.userData.isOpen) {
|
||||
// Create highlight ring
|
||||
const ringGeom = new THREE.RingGeometry(1.2, 1.4, 32);
|
||||
const ringMat = new THREE.MeshBasicMaterial({
|
||||
color: 0x00ff00,
|
||||
side: THREE.DoubleSide,
|
||||
transparent: true,
|
||||
opacity: 0.6
|
||||
});
|
||||
const ring = new THREE.Mesh(ringGeom, ringMat);
|
||||
ring.rotation.x = -Math.PI / 2;
|
||||
ring.position.copy(doorMesh.position);
|
||||
ring.position.y = 0.05;
|
||||
|
||||
|
||||
// Store reference to door for click handling
|
||||
doorMesh.userData.isExit = true;
|
||||
// Create proper exit data with all required fields
|
||||
const firstCell = doorMesh.userData.cells[0];
|
||||
// Convert numeric direction (0,1,2,3) to string ('N','E','S','W')
|
||||
const dirMap = { 0: 'N', 1: 'E', 2: 'S', 3: 'W' };
|
||||
doorMesh.userData.exitData = {
|
||||
x: firstCell.x,
|
||||
y: firstCell.y,
|
||||
direction: dirMap[doorMesh.userData.direction] || 'N'
|
||||
};
|
||||
|
||||
this.exitHighlightGroup.add(ring);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
showPlacementPreview(preview) {
|
||||
if (!preview) {
|
||||
this.hidePlacementPreview();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create preview groups if they don't exist
|
||||
if (!this.previewGroup) {
|
||||
this.previewGroup = new THREE.Group();
|
||||
this.scene.add(this.previewGroup);
|
||||
}
|
||||
|
||||
if (!this.projectionGroup) {
|
||||
this.projectionGroup = new THREE.Group();
|
||||
this.scene.add(this.projectionGroup);
|
||||
}
|
||||
|
||||
// Clear previous preview
|
||||
this.previewGroup.clear();
|
||||
this.projectionGroup.clear();
|
||||
|
||||
const { card, cells, isValid, x, y, rotation } = preview;
|
||||
|
||||
// Calculate bounds for tile - OLD LOGIC (Removed)
|
||||
// Note: We ignore 'cells' for positioning the texture, but keep them for the Ground Projection (Green/Red squares)
|
||||
|
||||
// 1. FLOATING TILE (Y = 3)
|
||||
if (card.textures && card.textures.length > 0) {
|
||||
this.getTexture(card.textures[0], (texture) => {
|
||||
|
||||
// Get Current Rotation Variant for Dimensions
|
||||
const currentVariant = card.variants[rotation];
|
||||
const rotWidth = currentVariant.width;
|
||||
const rotHeight = currentVariant.height;
|
||||
|
||||
// Calculate Center based on Anchor (x, y) and Dimensions
|
||||
const cx = x + (rotWidth - 1) / 2;
|
||||
const cy = y + (rotHeight - 1) / 2;
|
||||
|
||||
// Use BASE dimensions from NORTH variant
|
||||
const baseWidth = card.variants.N.width;
|
||||
const baseHeight = card.variants.N.height;
|
||||
|
||||
const geometry = new THREE.PlaneGeometry(baseWidth, baseHeight);
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
opacity: 0.8,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
const floatingTile = new THREE.Mesh(geometry, material);
|
||||
|
||||
floatingTile.rotation.x = -Math.PI / 2;
|
||||
|
||||
// Apply Z rotation
|
||||
const rotMap = { 'N': 0, 'E': 1, 'S': 2, 'W': 3 };
|
||||
const r = rotMap[rotation] !== undefined ? rotMap[rotation] : 0;
|
||||
floatingTile.rotation.z = -r * (Math.PI / 2);
|
||||
|
||||
console.log(`[Preview] Rotation: ${rotation}, Center: (${cx}, ${cy})`);
|
||||
|
||||
floatingTile.position.set(cx, 3, -cy);
|
||||
this.previewGroup.add(floatingTile);
|
||||
});
|
||||
}
|
||||
|
||||
// 2. GROUND PROJECTION (Green/Red)
|
||||
const projectionColor = isValid ? 0x00ff00 : 0xff0000;
|
||||
cells.forEach(cell => {
|
||||
const geometry = new THREE.PlaneGeometry(0.95, 0.95);
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
color: projectionColor,
|
||||
transparent: true,
|
||||
opacity: 0.5,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
const projection = new THREE.Mesh(geometry, material);
|
||||
projection.rotation.x = -Math.PI / 2;
|
||||
projection.position.set(cell.x, 0.02, -cell.y);
|
||||
this.projectionGroup.add(projection);
|
||||
});
|
||||
}
|
||||
|
||||
hidePlacementPreview() {
|
||||
if (this.previewGroup) {
|
||||
this.previewGroup.clear();
|
||||
}
|
||||
if (this.projectionGroup) {
|
||||
this.projectionGroup.clear();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -134,6 +134,137 @@ export class UIManager {
|
||||
|
||||
// Set initial active button (North)
|
||||
this.updateActiveViewButton(DIRECTIONS.NORTH);
|
||||
|
||||
// --- Tile Placement Controls (Bottom Center) ---
|
||||
this.placementPanel = document.createElement('div');
|
||||
this.placementPanel.style.position = 'absolute';
|
||||
this.placementPanel.style.bottom = '20px';
|
||||
this.placementPanel.style.left = '50%';
|
||||
this.placementPanel.style.transform = 'translateX(-50%)';
|
||||
this.placementPanel.style.display = 'none'; // Hidden by default
|
||||
this.placementPanel.style.pointerEvents = 'auto';
|
||||
this.placementPanel.style.backgroundColor = 'rgba(0, 0, 0, 0.85)';
|
||||
this.placementPanel.style.padding = '15px';
|
||||
this.placementPanel.style.borderRadius = '8px';
|
||||
this.placementPanel.style.border = '2px solid #666';
|
||||
this.container.appendChild(this.placementPanel);
|
||||
|
||||
// Status text
|
||||
this.placementStatus = document.createElement('div');
|
||||
this.placementStatus.style.color = '#fff';
|
||||
this.placementStatus.style.fontSize = '16px';
|
||||
this.placementStatus.style.fontFamily = 'sans-serif';
|
||||
this.placementStatus.style.marginBottom = '10px';
|
||||
this.placementStatus.style.textAlign = 'center';
|
||||
this.placementStatus.textContent = 'Coloca la loseta';
|
||||
this.placementPanel.appendChild(this.placementStatus);
|
||||
|
||||
// Controls container
|
||||
const placementControls = document.createElement('div');
|
||||
placementControls.style.display = 'flex';
|
||||
placementControls.style.gap = '15px';
|
||||
placementControls.style.alignItems = 'center';
|
||||
this.placementPanel.appendChild(placementControls);
|
||||
|
||||
// Movement arrows (4-way grid)
|
||||
const arrowGrid = document.createElement('div');
|
||||
arrowGrid.style.display = 'grid';
|
||||
arrowGrid.style.gridTemplateColumns = '40px 40px 40px';
|
||||
arrowGrid.style.gap = '3px';
|
||||
|
||||
const createArrow = (label, dx, dy) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = label;
|
||||
btn.style.width = '40px';
|
||||
btn.style.height = '40px';
|
||||
btn.style.backgroundColor = '#444';
|
||||
btn.style.color = '#fff';
|
||||
btn.style.border = '1px solid #888';
|
||||
btn.style.cursor = 'pointer';
|
||||
btn.style.fontSize = '18px';
|
||||
btn.onclick = () => {
|
||||
if (this.dungeon) {
|
||||
this.dungeon.movePlacement(dx, dy);
|
||||
}
|
||||
};
|
||||
return btn;
|
||||
};
|
||||
|
||||
const arrowUp = createArrow('↑', 0, 1);
|
||||
const arrowLeft = createArrow('←', -1, 0);
|
||||
const arrowRight = createArrow('→', 1, 0);
|
||||
const arrowDown = createArrow('↓', 0, -1);
|
||||
|
||||
arrowUp.style.gridColumn = '2';
|
||||
arrowLeft.style.gridColumn = '1';
|
||||
arrowRight.style.gridColumn = '3';
|
||||
arrowDown.style.gridColumn = '2';
|
||||
|
||||
arrowGrid.appendChild(arrowUp);
|
||||
arrowGrid.appendChild(arrowLeft);
|
||||
arrowGrid.appendChild(arrowRight);
|
||||
arrowGrid.appendChild(arrowDown);
|
||||
|
||||
placementControls.appendChild(arrowGrid);
|
||||
|
||||
// Rotate button
|
||||
this.rotateBtn = document.createElement('button');
|
||||
this.rotateBtn.textContent = '🔄 Rotar';
|
||||
this.rotateBtn.style.padding = '10px 20px';
|
||||
this.rotateBtn.style.backgroundColor = '#555';
|
||||
this.rotateBtn.style.color = '#fff';
|
||||
this.rotateBtn.style.border = '1px solid #888';
|
||||
this.rotateBtn.style.cursor = 'pointer';
|
||||
this.rotateBtn.style.fontSize = '16px';
|
||||
this.rotateBtn.style.borderRadius = '4px';
|
||||
this.rotateBtn.onclick = () => {
|
||||
if (this.dungeon) {
|
||||
this.dungeon.rotatePlacement();
|
||||
}
|
||||
};
|
||||
placementControls.appendChild(this.rotateBtn);
|
||||
|
||||
// Place button
|
||||
this.placeBtn = document.createElement('button');
|
||||
this.placeBtn.textContent = '⬇ Bajar';
|
||||
this.placeBtn.style.padding = '10px 20px';
|
||||
this.placeBtn.style.backgroundColor = '#2a5';
|
||||
this.placeBtn.style.color = '#fff';
|
||||
this.placeBtn.style.border = '1px solid #888';
|
||||
this.placeBtn.style.cursor = 'pointer';
|
||||
this.placeBtn.style.fontSize = '16px';
|
||||
this.placeBtn.style.borderRadius = '4px';
|
||||
this.placeBtn.onclick = () => {
|
||||
if (this.dungeon) {
|
||||
const success = this.dungeon.confirmPlacement();
|
||||
if (!success) {
|
||||
alert('❌ No se puede colocar la loseta en esta posición');
|
||||
}
|
||||
}
|
||||
};
|
||||
placementControls.appendChild(this.placeBtn);
|
||||
}
|
||||
|
||||
showPlacementControls(show) {
|
||||
if (this.placementPanel) {
|
||||
this.placementPanel.style.display = show ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
updatePlacementStatus(isValid) {
|
||||
if (this.placementStatus) {
|
||||
if (isValid) {
|
||||
this.placementStatus.textContent = '✅ Posición válida';
|
||||
this.placementStatus.style.color = '#0f0';
|
||||
this.placeBtn.style.backgroundColor = '#2a5';
|
||||
this.placeBtn.style.cursor = 'pointer';
|
||||
} else {
|
||||
this.placementStatus.textContent = '❌ Posición inválida';
|
||||
this.placementStatus.style.color = '#f44';
|
||||
this.placeBtn.style.backgroundColor = '#555';
|
||||
this.placeBtn.style.cursor = 'not-allowed';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateActiveViewButton(activeDirection) {
|
||||
@@ -201,13 +332,15 @@ export class UIManager {
|
||||
ctx.fillRect(cx, cy, cellSize, cellSize);
|
||||
}
|
||||
|
||||
// Draw Exits (Pending)
|
||||
// Draw Exits (Available)
|
||||
ctx.fillStyle = '#0f0'; // Green dots for open exits
|
||||
this.dungeon.pendingExits.forEach(exit => {
|
||||
const ex = centerX + (exit.x * cellSize);
|
||||
const ey = centerY - (exit.y * cellSize);
|
||||
ctx.fillRect(ex, ey, cellSize, cellSize);
|
||||
});
|
||||
if (this.dungeon.availableExits) {
|
||||
this.dungeon.availableExits.forEach(exit => {
|
||||
const ex = centerX + (exit.x * cellSize);
|
||||
const ey = centerY - (exit.y * cellSize);
|
||||
ctx.fillRect(ex, ey, cellSize, cellSize);
|
||||
});
|
||||
}
|
||||
|
||||
// Draw Entry (0,0) cross
|
||||
ctx.strokeStyle = '#f00';
|
||||
|
||||
Reference in New Issue
Block a user