Fix(GameEngine): Critical Room Reveal hang resolve and Devlog update
59
DEVLOG.md
@@ -1,5 +1,64 @@
|
||||
# Devlog - Warhammer Quest (Versión Web 3D)
|
||||
|
||||
## Sesión 14: Estabilización del Flujo de Juego y Bugfix Crítico
|
||||
**Fecha:** 10 de Enero de 2026
|
||||
|
||||
### Objetivos
|
||||
- Solucionar el bloqueo crítico al revelar nuevas estancias ("Estancia Revelada - Preparando encuentro...").
|
||||
- Refinar el sistema híbrido de notificaciones (Log persistente + Texto flotante).
|
||||
|
||||
### Cambios Realizados
|
||||
|
||||
#### 1. Corrección del Bloqueo en "Estancia Revelada"
|
||||
- **Síntoma**: Al entrar en una habitación nueva, el juego mostraba el mensaje de "Preparando encuentro..." pero se quedaba congelado en la fase de Héroes, impidiendo que los monstruos o el evento se activaran.
|
||||
- **Causa**: La lógica de eventos difería la resolución a la Fase de Monstruos (`pendingExploration`), pero el cambio de fase no se disparaba automáticamente si no había enemigos previos, dejando al juego en un estado indeterminado ("limbo").
|
||||
- **Solución**:
|
||||
- Se ha modificado `GameEngine.js` para resolver el evento de exploración **inmediatamente** al entrar en la sala (callback en `executeMovePath`).
|
||||
- Si surgen monstruos, se fuerza el cambio a Fase de Monstruos.
|
||||
- Si la sala está despejada, se mantiene la Fase de Héroes, permitiendo continuar el turno.
|
||||
- **Mejora en `EventInterpreter`**: Se añadió soporte para `callbacks` de finalización (`onComplete`), permitiendo un control de flujo más granular en lugar de depender ciegamente de `resumeFromEvent`.
|
||||
|
||||
#### 2. Sistema de Notificaciones Híbrido
|
||||
- **Consola Persistente**: Se ha refinado el panel de log en la esquina inferior izquierda (`FeedbackUI.js`) para mantener un historial de eventos de combate y reglas.
|
||||
- **Texto Flotante**: Se mantiene `renderer.showFloatingText` para feedback inmediato y efímero sobre las unidades (daño, estados).
|
||||
- **Integración**: `GameEngine` ahora redirigide los mensajes de eventos críticos a ambos sistemas según su importancia.
|
||||
|
||||
### Estado Actual
|
||||
El juego es ahora estable en el ciclo principal de exploración y combate. La transición entre descubrir una sala y combatir monstruos es fluida e inmediata, sin pausas extrañas ni bloqueos.
|
||||
|
||||
---
|
||||
|
||||
## Sesión 13: Eventos de Exploración y Regla de Derrumbamiento
|
||||
**Fecha:** 9 de Enero de 2026
|
||||
|
||||
### Objetivos
|
||||
- Implementar la mecánica de "Derrumbamiento" (Collapse) con todas sus reglas asociadas.
|
||||
- Establecer el sistema de Eventos de Exploración al entrar en nuevas estancias ("Room Revealed").
|
||||
|
||||
### Cambios Realizados
|
||||
|
||||
#### 1. Mecánica de Derrumbamiento ("Collapse")
|
||||
- **Regla de "Sala Inicial"**: Si se roba la carta de Derrumbamiento en la sala de inicio (tile_0) o en el turno 1, se ignora y se roba otra carta automáticamente para evitar un "Game Over" inmediato antes de empezar.
|
||||
- **Bloqueo Visual de Salidas**: Implementada la función `collapseExits` que:
|
||||
- Identifica las salidas no abiertas de la sala actual.
|
||||
- Coloca marcadores visuales de "Escombros" y cambia la textura de las puertas a "Bloqueada".
|
||||
- Elimina lógicamente las salidas del generador de mazmorras.
|
||||
- Agrupa celdas de puerta adyacentes para reportar un conteo de puertas "humanas" (visuales) en lugar de celdas individuales en el log.
|
||||
- **Cuenta Atrás Mortal**: Implementado estado de `collapsingRoom`. Al final del siguiente turno, cualquier entidad (héroe o monstruo) que permanezca en la sala muere aplastada instantáneamente.
|
||||
- **Destrabarse Gratis**: Modificada la lógica `attemptBreakAway`. Si la sala se está derrumbando, los héroes ignoran las zonas de control de los monstruos y escapan automáticamente (éxito garantizado) para intentar salvar su vida.
|
||||
|
||||
#### 2. Eventos de Exploración (Nuevas Estancias)
|
||||
- **Detección de Entrada**: El sistema de movimiento (`executeMovePath`) ahora detecta si el héroe entra en una celda de tipo `ROOM` que no ha sido `visited`.
|
||||
- **Interrupción de Movimiento**: Al entrar en una habitación nueva, el héroe se detiene instantáneamente y pierde el resto de su movimiento ("Stop").
|
||||
- **Fase de Monstruos**: Al inicio de la Fase de Monstruos, si hay una exploración pendiente:
|
||||
- Se roba una carta del Mazo de Eventos.
|
||||
- Si es un Evento, se resuelve normalmente.
|
||||
- Si son Monstruos, se generan **DENTRO** de la sala recién revelada (gracias a la inyección de contexto `tileId` en `findSpawnPoints`).
|
||||
|
||||
### Estado Actual
|
||||
El juego ahora soporta situaciones de crisis extremas (Derrumbes) y la tensión natural de abrir puertas desconocidas. La exploración ya no es segura; cada nueva sala es una amenaza potencial que se activa en el turno de los monstruos.
|
||||
|
||||
|
||||
## Sesión 12 (Continuación): Refactorización y Renderizado (Intento II - Exitoso)
|
||||
**Fecha:** 9 de Enero de 2026
|
||||
|
||||
|
||||
BIN
public/assets/images/dungeon1/markers/rubbles.png
Normal file
|
After Width: | Height: | Size: 13 MiB |
BIN
public/assets/images/dungeon1/standees/enemies/araña_gigante.png
Normal file
|
After Width: | Height: | Size: 646 KiB |
BIN
public/assets/images/dungeon1/standees/enemies/minotaur.png
Normal file
|
After Width: | Height: | Size: 338 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
BIN
public/assets/images/dungeon1/tokens/enemies/minotaur.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
@@ -1,114 +1,251 @@
|
||||
export const EVENT_CARDS_DATA = [
|
||||
{
|
||||
"id": "evt_derrumbamiento",
|
||||
"titulo": "DERRUMBAMIENTO",
|
||||
"tipo": "Evento",
|
||||
"codigo_tipo": "E",
|
||||
"descripcion": "Todas las salidas, excepto por la que entraron los Aventureros, están bloqueadas. Al final del siguiente turno, cualquier miniatura en la estancia muere aplastada. La estancia queda intransitable.",
|
||||
"reglas_especiales": [
|
||||
"Colocar marcador de Derrumbamiento.",
|
||||
"Aventureros no sujetos a reglas de trabado por combate al intentar escapar.",
|
||||
"Los Monstruos en la estancia mueren automáticamente."
|
||||
],
|
||||
"cita_fuente": "[cite: 1, 2, 3, 4, 5]"
|
||||
"descripcion": "Todas las salidas, excepto por la que entraron los Aventureros, están bloqueadas.",
|
||||
"cita_fuente": "[cite: 1, 2, 3, 4, 5]",
|
||||
"acciones": [
|
||||
{
|
||||
"tipo_accion": "ENTORNO",
|
||||
"subtipo": "bloquear_salidas_excepto_entrada"
|
||||
},
|
||||
|
||||
{
|
||||
"tipo_accion": "MENSAJE",
|
||||
"texto": "¡Derrumbamiento! La estancia quedará intransitable al final del siguiente turno."
|
||||
},
|
||||
{
|
||||
"tipo_accion": "ESTADO_GLOBAL",
|
||||
"id": "cuenta_atras_derrumbamiento",
|
||||
"duracion_turnos": 1,
|
||||
"efecto_fin": "muerte_instantanea_en_loseta"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "evt_cadaver",
|
||||
"titulo": "CADÁVER",
|
||||
"tipo": "Evento",
|
||||
"codigo_tipo": "E",
|
||||
"descripcion": "Un Bárbaro muerto sostiene una bolsa de cuero. El Aventurero con el resultado más bajo en 1D6 coge la bolsa.",
|
||||
"tabla_efectos": [
|
||||
{ "resultado": "1", "efecto": "¡Gas venenoso! 1D6 Heridas (sin modificadores). Bolsa vacía." },
|
||||
{ "resultado": "2-3", "efecto": "¡Trampa! Lanza de pared inflige 2D6 Heridas al Aventurero. Bolsa vacía." },
|
||||
{ "resultado": "4-6", "efecto": "Tesoro. La bolsa contiene 1D6 x 100 monedas de oro." }
|
||||
],
|
||||
"notas": "Roba otra Carta de Evento inmediatamente después de resolver.",
|
||||
"cita_fuente": "[cite: 6, 7, 11, 15, 16]"
|
||||
"descripcion": "Un Bárbaro muerto sostiene una bolsa de cuero.",
|
||||
"cita_fuente": "[cite: 6, 7, 11, 15, 16]",
|
||||
"acciones": [
|
||||
{
|
||||
"tipo_accion": "SELECCION",
|
||||
"modo": "tirada_baja",
|
||||
"dado": "1D6",
|
||||
"guardar_como": "victima",
|
||||
"mensaje": "Cada jugador tira 1D6. El más bajo investiga el cadáver."
|
||||
},
|
||||
{
|
||||
"tipo_accion": "PRUEBA",
|
||||
"origen": "victima",
|
||||
"tipo_prueba": "1D6",
|
||||
"tabla": {
|
||||
"1": [
|
||||
{ "tipo_accion": "EFECTO", "objetivo": "victima", "tipo": "daño", "cantidad": "1D6", "mensaje": "¡Gas venenoso!" },
|
||||
{ "tipo_accion": "MENSAJE", "texto": "La bolsa estaba vacía." }
|
||||
],
|
||||
"2-3": [
|
||||
{ "tipo_accion": "EFECTO", "objetivo": "victima", "tipo": "daño", "cantidad": "2D6", "mensaje": "¡Trampa de lanza!" },
|
||||
{ "tipo_accion": "MENSAJE", "texto": "La bolsa estaba vacía." }
|
||||
],
|
||||
"4-6": [
|
||||
{ "tipo_accion": "EFECTO", "objetivo": "victima", "tipo": "oro", "cantidad": "1D6*100", "mensaje": "¡Encuentras oro!" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tipo_accion": "TRIGGER_EVENTO",
|
||||
"cantidad": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "evt_viejos_huesos",
|
||||
"titulo": "VIEJOS HUESOS",
|
||||
"tipo": "Evento",
|
||||
"codigo_tipo": "E",
|
||||
"descripcion": "Suelo cubierto de huesos y cráneos con brillo de monedas bajo ellos.",
|
||||
"tabla_efectos": [
|
||||
{ "resultado": "1", "efecto": "Ilusión. Los huesos y el oro desaparecen. Roba una Carta de Evento inmediatamente." },
|
||||
{ "resultado": "2-3", "efecto": "Rayo mágico. Un Aventurero al azar sufre 1D6 Heridas (sin modificadores). Roba una Carta de Evento inmediatamente." },
|
||||
{ "resultado": "4-5", "efecto": "Cada Aventurero en la sección encuentra 1D6 x 10 monedas de oro. Roba otra Carta de Evento." },
|
||||
{ "resultado": "6", "efecto": "Cada Aventurero encuentra 2D6 x 10 monedas de oro y roba una Carta de Tesoro." }
|
||||
],
|
||||
"cita_fuente": "[cite: 8, 9, 12, 14, 17, 18]"
|
||||
"cita_fuente": "[cite: 8, 9, 12, 14, 17, 18]",
|
||||
"acciones": [
|
||||
{
|
||||
"tipo_accion": "PRUEBA",
|
||||
"tipo_prueba": "1D6_GLOBAL",
|
||||
"mensaje": "Tira 1D6 para ver qué esconden los huesos.",
|
||||
"tabla": {
|
||||
"1": [
|
||||
{ "tipo_accion": "MENSAJE", "texto": "Ilusión. Los huesos y el oro desaparecen." },
|
||||
{ "tipo_accion": "TRIGGER_EVENTO", "cantidad": 1 }
|
||||
],
|
||||
"2-3": [
|
||||
{ "tipo_accion": "SELECCION", "modo": "azar", "guardar_como": "victima" },
|
||||
{ "tipo_accion": "EFECTO", "objetivo": "victima", "tipo": "daño", "cantidad": "1D6", "mensaje": "¡Rayo mágico!" },
|
||||
{ "tipo_accion": "TRIGGER_EVENTO", "cantidad": 1 }
|
||||
],
|
||||
"4-5": [
|
||||
{ "tipo_accion": "SELECCION", "modo": "todos" },
|
||||
{ "tipo_accion": "EFECTO", "tipo": "oro", "cantidad": "1D6*10", "mensaje": "Cada aventurero encuentra monedas." },
|
||||
{ "tipo_accion": "TRIGGER_EVENTO", "cantidad": 1 }
|
||||
],
|
||||
"6": [
|
||||
{ "tipo_accion": "SELECCION", "modo": "todos" },
|
||||
{ "tipo_accion": "EFECTO", "tipo": "oro", "cantidad": "2D6*10", "mensaje": "¡Un gran hallazgo de oro!" },
|
||||
{ "tipo_accion": "EFECTO", "tipo": "carta_tesoro", "cantidad": 1, "mensaje": "Robas una Carta de Tesoro." }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "evt_trampa",
|
||||
"titulo": "TRAMPA",
|
||||
"tipo": "Evento",
|
||||
"codigo_tipo": "E",
|
||||
"descripcion": "El Aventurero con el resultado menor en 1D6 activa una trampa.",
|
||||
"tabla_efectos": [
|
||||
{ "resultado": "1", "efecto": "Explosión. Todas las miniaturas en la sección sufren 1D6 Heridas (sin modificadores)." },
|
||||
{ "resultado": "2-5", "efecto": "Grieta. El Aventurero cae al subsuelo y sufre 2D6 Heridas. Solo escapa con Cuerda o Hechizo Levitar." },
|
||||
{ "resultado": "6", "efecto": "Tesoro oculto. Roba una Carta de Tesoro. Con 1-3 en 1D6, roba otro Evento." }
|
||||
],
|
||||
"cita_fuente": "[cite: 19, 20, 21, 22, 23, 24]"
|
||||
"descripcion": "El Aventurero con el resultado menor activa una trampa.",
|
||||
"cita_fuente": "[cite: 19, 20, 21, 22, 23, 24]",
|
||||
"acciones": [
|
||||
{
|
||||
"tipo_accion": "SELECCION",
|
||||
"modo": "tirada_baja",
|
||||
"dado": "1D6",
|
||||
"guardar_como": "victima",
|
||||
"mensaje": "¡Click! Alguien ha pisado algo..."
|
||||
},
|
||||
{
|
||||
"tipo_accion": "PRUEBA",
|
||||
"origen": "victima",
|
||||
"tipo_prueba": "1D6",
|
||||
"tabla": {
|
||||
"1": [
|
||||
{ "tipo_accion": "SELECCION", "modo": "todos" },
|
||||
{ "tipo_accion": "EFECTO", "tipo": "daño", "cantidad": "1D6", "mensaje": "¡Explosión! Todos sufren daño." }
|
||||
],
|
||||
"2-5": [
|
||||
{ "tipo_accion": "EFECTO", "objetivo": "victima", "tipo": "daño", "cantidad": "2D6", "mensaje": "¡Caes por una grieta!" },
|
||||
{ "tipo_accion": "MENSAJE", "texto": "Solo escapas con Cuerda o Hechizo Levitar (Lógica pendiente)." }
|
||||
],
|
||||
"6": [
|
||||
{ "tipo_accion": "EFECTO", "objetivo": "victima", "tipo": "carta_tesoro", "cantidad": 1, "mensaje": "¡Era una trampa falsa! Encuentras un tesoro oculto." },
|
||||
{
|
||||
"tipo_accion": "PRUEBA", "tipo_prueba": "1D6", "tabla": {
|
||||
"1-3": [{ "tipo_accion": "TRIGGER_EVENTO", "cantidad": 1 }]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "evt_rastrillo",
|
||||
"titulo": "RASTRILLO",
|
||||
"tipo": "Evento",
|
||||
"codigo_tipo": "E",
|
||||
"descripcion": "Un rastrillo baja al entrar todos los Aventureros, bloqueando la salida de escape.",
|
||||
"reglas_especiales": [
|
||||
"Solo podrán regresar por ese camino si tienen la llave.",
|
||||
"Colocar marcador de rastrillo en la puerta de entrada.",
|
||||
"Roba otra Carta de Evento inmediatamente."
|
||||
],
|
||||
"cita_fuente": "[cite: 25, 26, 27]"
|
||||
},
|
||||
{
|
||||
"titulo": "ESCORPIONES",
|
||||
"tipo": "Evento/Monstruo",
|
||||
"codigo_tipo": "E",
|
||||
"descripcion": "Un enjambre de 12 escorpiones pequeños ataca a un Aventurero al azar.",
|
||||
"mecanica": {
|
||||
"cantidad_inicial": 12,
|
||||
"ataque_jugador": "1D6 + Fuerza = número de escorpiones eliminados.",
|
||||
"ataque_enemigo": "Cada escorpión restante inflige 1 Herida (sin modificadores).",
|
||||
"recompensa": "5 monedas de oro por escorpión muerto."
|
||||
},
|
||||
"notas": "El enjambre se aleja tras el ataque y la carta se descarta.",
|
||||
"cita_fuente": "[cite: 28, 29, 30, 31, 32, 33, 34]"
|
||||
"descripcion": "Un rastrillo baja bloqueando la salida.",
|
||||
"cita_fuente": "[cite: 25, 26, 27]",
|
||||
"acciones": [
|
||||
{
|
||||
"tipo_accion": "ENTORNO",
|
||||
"subtipo": "bloquear_salida_entrada"
|
||||
},
|
||||
{
|
||||
"tipo_accion": "ENTORNO",
|
||||
"subtipo": "colocar_marcador",
|
||||
"marcador": "rastrillo",
|
||||
"posicion": "entrada"
|
||||
},
|
||||
{
|
||||
"tipo_accion": "TRIGGER_EVENTO",
|
||||
"cantidad": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
//{
|
||||
// "id": "evt_escorpiones",
|
||||
// "titulo": "ESCORPIONES",
|
||||
// "tipo": "Evento/Monstruo",
|
||||
// "codigo_tipo": "E",
|
||||
// "descripcion": "Un enjambre de escorpiones ataca.",
|
||||
// "cita_fuente": "[cite: 28, 29, 30, 31, 32, 33, 34]",
|
||||
// "acciones": [
|
||||
// {
|
||||
// "tipo_accion": "SELECCION",
|
||||
// "modo": "azar",
|
||||
// "guardar_como": "victima",
|
||||
// "mensaje": "¡Los escorpiones eligen una presa!"
|
||||
// },
|
||||
// {
|
||||
// "tipo_accion": "MINIJUEGO",
|
||||
// "id_logica": "enjambre_escorpiones", // Lógica custom para calcular daño vs fuerza
|
||||
// "cantidad": 12,
|
||||
// "objetivo": "victima"
|
||||
// }
|
||||
// ]
|
||||
//},
|
||||
{
|
||||
"id": "mon_minotauro",
|
||||
"titulo": "MINOTAURO",
|
||||
"tipo": "Monstruo",
|
||||
"codigo_tipo": "M",
|
||||
"estadisticas": {
|
||||
"heridas": 15,
|
||||
"movimiento": 6,
|
||||
"habilidad_armas": 4,
|
||||
"fuerza": 4,
|
||||
"resistencia": 4,
|
||||
"ataques": 2
|
||||
},
|
||||
"reglas_especiales": [
|
||||
"Causa Miedo.",
|
||||
"Si impacta a un Aventurero, inflige 2D6 + 4 Heridas.",
|
||||
"Roba otra Carta de Evento antes de luchar (si salen adversarios, combaten a la vez)."
|
||||
],
|
||||
"valor_oro": 440
|
||||
"descripcion": "Un poderoso Minotauro bloquea el paso.",
|
||||
"acciones": [
|
||||
{
|
||||
"tipo_accion": "SPAWN",
|
||||
"id_monstruo": "minotaur", // Debe coincidir con MONSTER_DEFINITIONS key o similar
|
||||
"nombre_fallback": "Minotauro", // Por si no existe id
|
||||
"cantidad": "1",
|
||||
"stats": { // Opcional, si queremos sobreescribir o definir aquí
|
||||
"wounds": 15, "move": 6, "ws": 4, "str": 4, "toughness": 4, "attacks": 2, "gold": 440
|
||||
}
|
||||
},
|
||||
{
|
||||
"tipo_accion": "TRIGGER_EVENTO",
|
||||
"cantidad": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "mon_chaosWarrior",
|
||||
"titulo": "GUERRERO DE CAOS",
|
||||
"tipo": "Monstruo",
|
||||
"codigo_tipo": "M",
|
||||
"descripcion": "Un guerrero de caos bloquea el paso.",
|
||||
"acciones": [
|
||||
{
|
||||
"tipo_accion": "SPAWN",
|
||||
"id_monstruo": "chaos_warrior", // Debe coincidir con MONSTER_DEFINITIONS key o similar
|
||||
"nombre_fallback": "Guerrero de Caos", // Por si no existe id
|
||||
"cantidad": "1D6-2",
|
||||
"stats": { // Opcional, si queremos sobreescribir o definir aquí
|
||||
"wounds": 10, "move": 4, "ws": 4, "str": 4, "toughness": 4, "attacks": 2, "gold": 240
|
||||
}
|
||||
},
|
||||
{
|
||||
"tipo_accion": "TRIGGER_EVENTO",
|
||||
"cantidad": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "mon_aranas",
|
||||
"titulo": "2D6 ARAÑAS GIGANTES",
|
||||
"tipo": "Monstruo",
|
||||
"codigo_tipo": "M",
|
||||
"estadisticas": {
|
||||
"heridas": 1,
|
||||
"movimiento": 6,
|
||||
"habilidad_armas": 2,
|
||||
"fuerza": "Especial",
|
||||
"resistencia": 2,
|
||||
"ataques": 1
|
||||
},
|
||||
"reglas_especiales": [
|
||||
"Ataque Telaraña: Si el objetivo está atrapado, la picadura inflige 1D3 Heridas automáticas.",
|
||||
"Si no está atrapado, la araña intenta impactar. Si lo logra, el Aventurero queda atrapado y no puede actuar hasta liberarse."
|
||||
"descripcion": "Descienden del techo...",
|
||||
"acciones": [
|
||||
{
|
||||
"tipo_accion": "SPAWN",
|
||||
"id_monstruo": "giant_spider",
|
||||
"nombre_fallback": "Araña Gigante",
|
||||
"cantidad": "2D6",
|
||||
"stats": {
|
||||
"wounds": 1, "move": 6, "ws": 2, "str": 3, "toughness": 2, "attacks": 1
|
||||
},
|
||||
"reglas_especiales": ["telarana"]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -6,19 +6,8 @@ export const EVENT_TYPES = {
|
||||
};
|
||||
|
||||
export const createEventDeck = () => {
|
||||
// Convert raw JSON data to engine-compatible card objects
|
||||
const deck = EVENT_CARDS_DATA.map(data => {
|
||||
const isMonster = data.tipo.includes('Monstruo');
|
||||
|
||||
return {
|
||||
id: `evt_${Math.random().toString(36).substr(2, 9)}`,
|
||||
type: isMonster ? EVENT_TYPES.MONSTER : EVENT_TYPES.EVENT,
|
||||
name: data.titulo,
|
||||
description: data.descripcion || data.titulo,
|
||||
data: data // Keep full raw data for specific logic (stats, tables)
|
||||
};
|
||||
});
|
||||
|
||||
// Return a deep copy of the definition data to avoid mutation issues
|
||||
const deck = EVENT_CARDS_DATA.map(card => JSON.parse(JSON.stringify(card)));
|
||||
return shuffleDeck(deck);
|
||||
};
|
||||
|
||||
@@ -27,5 +16,16 @@ const shuffleDeck = (deck) => {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[deck[i], deck[j]] = [deck[j], deck[i]];
|
||||
}
|
||||
|
||||
// DEBUG: Move Derrumbamiento to TOP
|
||||
/*
|
||||
const cardIdx = deck.findIndex(c => c.id === 'evt_derrumbamiento');
|
||||
if (cardIdx !== -1) {
|
||||
const card = deck.splice(cardIdx, 1)[0];
|
||||
deck.unshift(card);
|
||||
console.log("DEBUG: Forced DERRUMBAMIENTO to top of deck.");
|
||||
}
|
||||
*/
|
||||
|
||||
return deck;
|
||||
};
|
||||
|
||||
380
src/engine/events/EventInterpreter.js
Normal file
@@ -0,0 +1,380 @@
|
||||
/**
|
||||
* EventInterpreter.js
|
||||
*
|
||||
* Takes high-level Action Instructions from Event Cards (JSON)
|
||||
* and executes them using the Game Engine's low-level systems.
|
||||
*/
|
||||
|
||||
export class EventInterpreter {
|
||||
constructor(gameEngine) {
|
||||
this.game = gameEngine;
|
||||
this.queue = [];
|
||||
this.isProcessing = false;
|
||||
this.currentContext = {}; // Store temporary variables (e.g. "victim")
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point to execute a card's actions
|
||||
* @param {Object} card The event card data
|
||||
*/
|
||||
async processEvent(card, onComplete = null) {
|
||||
if (!card.acciones || !Array.isArray(card.acciones)) {
|
||||
console.warn("[EventInterpreter] Card has no actions:", card);
|
||||
if (onComplete) onComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[EventInterpreter] Processing ${card.titulo}`);
|
||||
|
||||
// Log Entry (System Trace)
|
||||
if (this.game.onShowMessage) {
|
||||
this.game.onShowMessage("Evento de Poder", `${card.titulo}`);
|
||||
}
|
||||
|
||||
// STEP 1: Show the Card to the User
|
||||
await this.showEventCard(card);
|
||||
|
||||
// Load actions into queue
|
||||
this.queue = [...card.acciones];
|
||||
this.processQueue(onComplete);
|
||||
}
|
||||
|
||||
// Helper wrapper for async UI
|
||||
async showEventCard(card) {
|
||||
return new Promise(resolve => {
|
||||
if (this.game.onShowEvent) {
|
||||
this.game.onShowEvent(card, resolve);
|
||||
} else {
|
||||
console.warn("[EventInterpreter] onShowEvent not bound, auto-resolving");
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async showModal(title, text) {
|
||||
return new Promise(resolve => {
|
||||
// Use GameEngine's generic message system IF it supports callback-based waiting
|
||||
// Currently onShowMessage is usually non-blocking temporary.
|
||||
// We need a blocking Modal.
|
||||
// Let's assume onShowEvent can be reused or we use a specific one.
|
||||
// For now, let's reuse onShowEvent with a generic structure or define a new callback.
|
||||
// Actually, we can just construct a mini-card object for onShowEvent
|
||||
const miniCard = {
|
||||
titulo: title,
|
||||
texto: text,
|
||||
tipo: 'Resolución'
|
||||
};
|
||||
if (this.game.onShowEvent) {
|
||||
this.game.onShowEvent(miniCard, resolve);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async processQueue(onComplete = null) {
|
||||
if (this.isProcessing) return;
|
||||
if (this.queue.length === 0) {
|
||||
console.log("[EventInterpreter] All actions completed.");
|
||||
|
||||
// Event Log Summary
|
||||
if (this.game.onShowMessage) {
|
||||
// We use onShowMessage as Log (via main.js filtering) or specialized logger
|
||||
// Actually, let's inject a "System Log" call if possible.
|
||||
// Main.js redirects "Efecto" titles to log, but let's be explicit.
|
||||
// Using generic onShowMessage with a distinct title for logging.
|
||||
// Or better, let's call a log method if exposed on game.
|
||||
|
||||
// Let's assume onShowMessage("LOG", ...) goes to log if we tweak main.js,
|
||||
// OR we just use a title that main.js recognizes as loggable.
|
||||
// But wait, the USER wants a log of the CARD itself.
|
||||
}
|
||||
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
} else {
|
||||
this.game.turnManager.resumeFromEvent(); // Resume turn flow
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.isProcessing = true;
|
||||
const action = this.queue.shift();
|
||||
|
||||
try {
|
||||
await this.executeAction(action);
|
||||
} catch (error) {
|
||||
console.error("[EventInterpreter] Error executing action:", error);
|
||||
}
|
||||
|
||||
this.isProcessing = false;
|
||||
|
||||
// Next step
|
||||
if (this.queue.length > 0) {
|
||||
setTimeout(() => this.processQueue(onComplete), 500);
|
||||
} else {
|
||||
this.processQueue(onComplete); // Finish up
|
||||
}
|
||||
}
|
||||
|
||||
async executeAction(action) {
|
||||
switch (action.tipo_accion) {
|
||||
case 'MENSAJE':
|
||||
await this.showModal("Evento", action.texto || action.mensaje);
|
||||
break;
|
||||
|
||||
case 'SELECCION':
|
||||
await this.handleSelection(action);
|
||||
break;
|
||||
|
||||
case 'PRUEBA':
|
||||
await this.handleTest(action);
|
||||
break;
|
||||
|
||||
case 'EFECTO':
|
||||
await this.handleEffect(action);
|
||||
break;
|
||||
|
||||
case 'SPAWN':
|
||||
await this.handleSpawn(action);
|
||||
break;
|
||||
|
||||
case 'ENTORNO':
|
||||
await this.handleEnvironment(action);
|
||||
break;
|
||||
|
||||
case 'ESTADO_GLOBAL':
|
||||
if (!this.game.state) this.game.state = {};
|
||||
this.game.state[action.clave] = action.valor;
|
||||
console.log(`[Event] Global State Update: ${action.clave} = ${action.valor}`);
|
||||
break;
|
||||
|
||||
case 'TRIGGER_EVENTO':
|
||||
console.log("TODO: Chain Event Draw");
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn("Unknown Action:", action.tipo_accion);
|
||||
}
|
||||
}
|
||||
|
||||
async handleSelection(action) {
|
||||
let targets = [];
|
||||
|
||||
if (action.modo === 'todos') {
|
||||
targets = [...this.game.heroes];
|
||||
} else if (action.modo === 'azar') {
|
||||
const idx = Math.floor(Math.random() * this.game.heroes.length);
|
||||
targets = [this.game.heroes[idx]];
|
||||
} else if (action.modo === 'tirada_baja') {
|
||||
// Roll D6 for each, pick lowest
|
||||
// For now, SIMULATED logic without UI prompts
|
||||
let lowest = 99;
|
||||
let candidates = [];
|
||||
|
||||
this.game.heroes.forEach(h => {
|
||||
const roll = Math.floor(Math.random() * 6) + 1;
|
||||
// console.log(`${h.name} rolled ${roll}`);
|
||||
if (roll < lowest) {
|
||||
lowest = roll;
|
||||
candidates = [h];
|
||||
} else if (roll === lowest) {
|
||||
candidates.push(h); // Tie
|
||||
}
|
||||
});
|
||||
// If tie, pick random from candidates
|
||||
targets = [candidates[Math.floor(Math.random() * candidates.length)]];
|
||||
}
|
||||
|
||||
// Store result
|
||||
if (action.guardar_como) {
|
||||
this.currentContext[action.guardar_como] = targets[0]; // Simplification for Single Target
|
||||
}
|
||||
|
||||
if (action.mensaje) {
|
||||
const names = targets.map(t => t.name).join(", ");
|
||||
// BLOCKING MODAL
|
||||
await this.showModal("Selección", `${action.mensaje}<br><br><b>Objetivo: ${names}</b>`);
|
||||
}
|
||||
}
|
||||
|
||||
async handleTest(action) {
|
||||
// Resolve target
|
||||
const target = action.origen ? this.currentContext[action.origen] : null;
|
||||
if (!target && action.origen) {
|
||||
console.error("Test target not found in context:", action.origen);
|
||||
return;
|
||||
}
|
||||
|
||||
let roll = 0;
|
||||
// Parse dice string "1D6", "2D6"
|
||||
if (action.tipo_prueba.includes('D6')) {
|
||||
const count = parseInt(action.tipo_prueba) || 1;
|
||||
for (let i = 0; i < count; i++) roll += Math.floor(Math.random() * 6) + 1;
|
||||
}
|
||||
|
||||
// BLOCKING MODAL: Show the roll result
|
||||
const targetName = target ? target.name : "Nadie";
|
||||
await this.showModal("Prueba", `<b>${targetName}</b> realiza una prueba de <b>${action.tipo_prueba}</b>...<br>Resultado: <b>${roll}</b>`);
|
||||
|
||||
// Check Table
|
||||
if (action.tabla) {
|
||||
let resultActions = null;
|
||||
|
||||
// Iterate keys "1", "2-3", "4-6"
|
||||
for (const key of Object.keys(action.tabla)) {
|
||||
if (key.includes('-')) {
|
||||
const [min, max] = key.split('-').map(Number);
|
||||
if (roll >= min && roll <= max) resultActions = action.tabla[key];
|
||||
} else {
|
||||
if (roll === Number(key)) resultActions = action.tabla[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (resultActions) {
|
||||
// Prepend these new actions to the FRONT of the queue to execute immediately
|
||||
console.log(`Test Roll: ${roll} -> Result found`, resultActions);
|
||||
this.queue.unshift(...resultActions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleEffect(action) {
|
||||
// Resolve Target
|
||||
let targets = [];
|
||||
if (action.objetivo) {
|
||||
if (this.currentContext[action.objetivo]) targets = [this.currentContext[action.objetivo]];
|
||||
} else {
|
||||
// Implicit context? Default to all?
|
||||
// Better to be explicit in JSON mostly
|
||||
}
|
||||
|
||||
// Parse Count
|
||||
let amount = 0;
|
||||
let isDice = false;
|
||||
if (typeof action.cantidad === 'number') amount = action.cantidad;
|
||||
else if (typeof action.cantidad === 'string' && action.cantidad.includes('D6')) {
|
||||
isDice = true;
|
||||
const parts = action.cantidad.split('*');
|
||||
const dicePart = parts[0];
|
||||
const multiplier = parts[1] ? parseInt(parts[1]) : 1;
|
||||
let roll = 0;
|
||||
const numDice = parseInt(dicePart) || 1;
|
||||
for (let i = 0; i < numDice; i++) roll += Math.floor(Math.random() * 6) + 1;
|
||||
amount = roll * multiplier;
|
||||
}
|
||||
|
||||
let msg = action.mensaje || "Efecto aplicado";
|
||||
if (isDice) msg += ` (Dado: ${amount})`;
|
||||
|
||||
if (action.tipo === 'daño') {
|
||||
targets.forEach(h => {
|
||||
// Apply Damage Logic
|
||||
// this.game.combatSystem.applyDamage(h, amount)...
|
||||
console.log(`Applying ${amount} Damage to ${h.name}`);
|
||||
h.stats.wounds -= amount; // Simple direct manipulation for now
|
||||
if (this.game.onEntityUpdate) this.game.onEntityUpdate(h);
|
||||
// Visual feedback?
|
||||
});
|
||||
await this.showModal("Daño", `${msg}<br><b>${amount} Heridas</b> a ${targets.map(t => t.name).join(", ")}`);
|
||||
} else if (action.tipo === 'oro') {
|
||||
targets.forEach(h => {
|
||||
console.log(`Giving ${amount} Gold to ${h.name}`);
|
||||
h.gold = (h.gold || 0) + amount;
|
||||
if (this.game.onEntityUpdate) this.game.onEntityUpdate(h);
|
||||
});
|
||||
await this.showModal("Oro", `${msg}<br>Ganan <b>${amount}</b> de Oro.`);
|
||||
}
|
||||
}
|
||||
|
||||
async handleSpawn(action) {
|
||||
// Parse Amount
|
||||
let count = 1;
|
||||
if (typeof action.cantidad === 'string' && action.cantidad.includes('D6')) {
|
||||
const numDice = parseInt(action.cantidad) || 1;
|
||||
for (let i = 0; i < numDice; i++) count += Math.floor(Math.random() * 6) + 1;
|
||||
} else {
|
||||
count = parseInt(action.cantidad) || 1;
|
||||
}
|
||||
|
||||
// Resolve ID/Stats
|
||||
// Map to Monster Definitions?
|
||||
// For now, construct dynamic definition
|
||||
const def = {
|
||||
name: action.nombre_fallback || "Enemigo",
|
||||
// Texture mapping based on ID
|
||||
portrait: this.getTexturePathForMonster(action.id_monstruo),
|
||||
stats: action.stats || { wounds: 1, move: 4, ws: 3, str: 3, toughness: 3 }
|
||||
};
|
||||
|
||||
// Check if there is an Exploration Context (TileID)
|
||||
let contextTileId = null;
|
||||
if (this.game.currentEventContext && this.game.currentEventContext.tileId) {
|
||||
contextTileId = this.game.currentEventContext.tileId;
|
||||
}
|
||||
|
||||
const spots = this.game.findSpawnPoints(count, contextTileId);
|
||||
|
||||
spots.forEach(spot => {
|
||||
// Monsters spawned via Event Card in Exploration phase should NOT skip turn?
|
||||
// "Monsters placed... move and attack as described in Power Phase" -> Wait.
|
||||
// Power Phase monsters ATTACK immediately. Exploration monsters DO NOT?
|
||||
// Rules say: "If Monsters, place them... see Power Phase for details"
|
||||
// Actually, "In the Monster Phase... determine how many... place them... Keep the card handy... information needed later"
|
||||
// Usually ambushes act immediately, but room dwellers act in the Monster Phase.
|
||||
// Since we are triggering this AT THE START of Monster Phase, they will act in this phase naturally ONLY IF we don't skip turn.
|
||||
|
||||
// However, GameEngine monster AI loop iterates all monsters.
|
||||
// Newly added monster might be picked up immediately if added to the array?
|
||||
// Yes, standard is they act.
|
||||
this.game.spawnMonster(def, spot.x, spot.y, { skipTurn: false });
|
||||
});
|
||||
|
||||
// Clear context after use to prevent leakage?
|
||||
// Or keep it for the duration of the event chain?
|
||||
// Safest to keep until event ends, GameEngine clears it?
|
||||
// We leave it, GameEngine overrides or clears it when needed.
|
||||
|
||||
await this.showModal("¡Emboscada!", `Aparecen <b>${count} ${def.name}</b>!<br>¡Prepárate para luchar!`);
|
||||
}
|
||||
|
||||
async handleEnvironment(action) {
|
||||
if (action.subtipo === 'bloquear_salidas_excepto_entrada') {
|
||||
console.log("[Event] Collapsing Exits...");
|
||||
if (this.game.collapseExits) {
|
||||
const collapsedCount = this.game.collapseExits();
|
||||
await this.showModal("¡Derrumbe!", `¡El techo se viene abajo!<br><b>${collapsedCount}</b> salidas han quedado bloqueadas por escombros.`);
|
||||
} else {
|
||||
console.error("GameEngine.collapseExits not implemented!");
|
||||
await this.showModal("Error", "GameEngine.collapseExits no implementado.");
|
||||
}
|
||||
} else if (action.subtipo === 'colocar_marcador') {
|
||||
console.log(`[Event] Placing Marker: ${action.marcador}`);
|
||||
if (this.game.placeEventMarker) {
|
||||
// Determine position based on context (e.g. center of current room)
|
||||
// For now, pass null so GameEngine decides based on player location
|
||||
this.game.placeEventMarker(action.marcador);
|
||||
}
|
||||
} else {
|
||||
console.log("Environment Action Unknown:", action.subtipo);
|
||||
}
|
||||
}
|
||||
|
||||
getTexturePathForMonster(id) {
|
||||
// Map JSON IDs to Files
|
||||
// id_monstruo: "minotaur" -> "dungeon1/standees/enemies/minotaur.png" (if exists) or fallback
|
||||
// We checked file list earlier:
|
||||
// bat.png, goblin.png, orc.png, skaven.png, chaosWarrior.png, Lordwarlock.png, rat.png, spiderGiant.png
|
||||
|
||||
const map = {
|
||||
"giant_spider": "spiderGiant.png",
|
||||
"orc": "orc.png",
|
||||
"goblin": "goblin.png",
|
||||
"skaven": "skaven.png",
|
||||
"minotaur": "minotaur.png"
|
||||
};
|
||||
|
||||
const filename = map[id] || "orc.png";
|
||||
return `assets/images/dungeon1/standees/enemies/${filename}`;
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ export class CombatMechanics {
|
||||
|
||||
if (log.hitRoll < log.targetToHit) {
|
||||
log.hitSuccess = false;
|
||||
log.message = `${attacker.name} falla el ataque (Sacó ${log.hitRoll}, necesita ${log.targetToHit}+).`;
|
||||
log.message = `💨 ${attacker.name} falla el ataque (Sacó ${log.hitRoll}, necesita ${log.targetToHit}+).`;
|
||||
return log;
|
||||
}
|
||||
|
||||
@@ -70,11 +70,10 @@ export class CombatMechanics {
|
||||
damageSum += r;
|
||||
}
|
||||
|
||||
log.damageRoll = damageSum; // Just sum for simple log, or we could array it
|
||||
log.damageRoll = damageSum;
|
||||
log.damageTotal = damageSum + attStr;
|
||||
|
||||
// 4. Calculate Wounds
|
||||
// Wounds = (Dice + Str) - Toughness
|
||||
let wounds = log.damageTotal - defTough;
|
||||
if (wounds < 0) wounds = 0;
|
||||
|
||||
@@ -82,9 +81,9 @@ export class CombatMechanics {
|
||||
|
||||
// 5. Build Message
|
||||
if (wounds > 0) {
|
||||
log.message = `${attacker.name} impacta y causa ${wounds} heridas! (Daño ${log.damageTotal} - Res ${defTough})`;
|
||||
log.message = `⚔️ ${attacker.name} impacta y causa ${wounds} heridas! (Daño ${log.damageTotal} - Res ${defTough})`;
|
||||
} else {
|
||||
log.message = `${attacker.name} impacta pero no logra herir. (Daño ${log.damageTotal} vs Res ${defTough})`;
|
||||
log.message = `🛡️ ${attacker.name} impacta pero no logra herir. (Daño ${log.damageTotal} vs Res ${defTough})`;
|
||||
}
|
||||
|
||||
// 6. Apply Damage to Defender State
|
||||
@@ -92,9 +91,9 @@ export class CombatMechanics {
|
||||
|
||||
if (defender.isDead) {
|
||||
log.defenderDied = true;
|
||||
log.message += ` ¡${defender.name} ha muerto!`;
|
||||
log.message += ` 💀 ¡${defender.name} ha muerto!`;
|
||||
} else if (defender.isUnconscious) {
|
||||
log.message += ` ¡${defender.name} cae inconsciente!`;
|
||||
log.message += ` 💀 ¡${defender.name} cae inconsciente!`;
|
||||
}
|
||||
|
||||
return log;
|
||||
@@ -123,20 +122,19 @@ export class CombatMechanics {
|
||||
|
||||
if (hitRoll === 1) {
|
||||
log.hitSuccess = false;
|
||||
log.message = `${attacker.name} dispara y falla (1 es fallo automático)`;
|
||||
log.message = `💨 ${attacker.name} dispara y falla (1 es fallo automático)`;
|
||||
return log;
|
||||
}
|
||||
|
||||
if (hitRoll < toHitTarget) {
|
||||
log.hitSuccess = false;
|
||||
log.message = `${attacker.name} dispara y falla (${hitRoll} < ${toHitTarget})`;
|
||||
log.message = `💨 ${attacker.name} dispara y falla (${hitRoll} < ${toHitTarget})`;
|
||||
return log;
|
||||
}
|
||||
|
||||
log.hitSuccess = true;
|
||||
|
||||
// 2. Roll Damage
|
||||
// Elf Bow Strength = 3
|
||||
const weaponStrength = 3;
|
||||
const damageRoll = this.rollD6();
|
||||
const damageTotal = weaponStrength + damageRoll;
|
||||
@@ -151,9 +149,9 @@ export class CombatMechanics {
|
||||
|
||||
// 4. Build Message
|
||||
if (wounds > 0) {
|
||||
log.message = `${attacker.name} acierta con el arco y causa ${wounds} heridas! (Daño ${log.damageTotal} - Res ${defTough})`;
|
||||
log.message = `⚔️ ${attacker.name} acierta con el arco y causa ${wounds} heridas! (Daño ${log.damageTotal} - Res ${defTough})`;
|
||||
} else {
|
||||
log.message = `${attacker.name} acierta pero la flecha rebota. (Daño ${log.damageTotal} vs Res ${defTough})`;
|
||||
log.message = `🛡️ ${attacker.name} acierta pero la flecha rebota. (Daño ${log.damageTotal} vs Res ${defTough})`;
|
||||
}
|
||||
|
||||
// 5. Apply Damage
|
||||
@@ -161,7 +159,7 @@ export class CombatMechanics {
|
||||
|
||||
if (defender.isDead) {
|
||||
log.defenderDied = true;
|
||||
log.message += ` ¡${defender.name} ha muerto!`;
|
||||
log.message += ` 💀 ¡${defender.name} ha muerto!`;
|
||||
}
|
||||
|
||||
return log;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { MonsterAI } from './MonsterAI.js';
|
||||
import { MagicSystem } from './MagicSystem.js';
|
||||
import { CombatSystem } from './CombatSystem.js';
|
||||
import { CombatMechanics } from './CombatMechanics.js';
|
||||
import { EventInterpreter } from '../events/EventInterpreter.js'; // Import
|
||||
import { HERO_DEFINITIONS } from '../data/Heroes.js';
|
||||
import { MONSTER_DEFINITIONS } from '../data/Monsters.js';
|
||||
import { createEventDeck, EVENT_TYPES } from '../data/Events.js';
|
||||
@@ -18,6 +19,7 @@ export class GameEngine {
|
||||
this.ai = new MonsterAI(this); // Init AI
|
||||
this.magicSystem = new MagicSystem(this); // Init Magic
|
||||
this.combatSystem = new CombatSystem(this); // Init Combat
|
||||
this.events = new EventInterpreter(this); // Init Events Engine
|
||||
this.player = null;
|
||||
this.selectedEntity = null;
|
||||
this.isRunning = false;
|
||||
@@ -36,18 +38,17 @@ export class GameEngine {
|
||||
this.onEntityDeath = null; // New: When entity dies
|
||||
this.onFloatingText = null; // New: For overhead text feedback
|
||||
this.onPathChange = null;
|
||||
this.onShowEvent = null; // New: For styled event cards
|
||||
}
|
||||
|
||||
startMission(missionConfig) {
|
||||
|
||||
this.dungeon.startDungeon(missionConfig);
|
||||
|
||||
// Create Party (4 Heroes)
|
||||
// Create Party (4 Heroes)
|
||||
this.createParty();
|
||||
|
||||
this.isRunning = true;
|
||||
this.turnManager.startGame();
|
||||
|
||||
// Listen for Phase Changes to Reset Moves
|
||||
this.turnManager.on('phase_changed', (phase) => {
|
||||
if (phase === 'hero' || phase === 'exploration') {
|
||||
@@ -61,6 +62,7 @@ export class GameEngine {
|
||||
window.RENDERER.clearAllActiveRings();
|
||||
}
|
||||
this.deselectEntity();
|
||||
this.ai.executeTurn();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -72,12 +74,20 @@ export class GameEngine {
|
||||
// 6. Listen for Power Phase Events
|
||||
this.turnManager.on('POWER_RESULT', (data) => {
|
||||
if (data.eventTriggered) {
|
||||
console.log("[GameEngine] Power Event Triggered! Waiting to handle...");
|
||||
// Determine if we need to draw a card or if it's a specific message
|
||||
setTimeout(() => this.handlePowerEvent(), 1500);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Initial Light Update
|
||||
setTimeout(() => this.updateLighting(), 500);
|
||||
|
||||
// Start Game Loop (Now that listeners are ready)
|
||||
this.isRunning = true;
|
||||
this.turnManager.startGame();
|
||||
}
|
||||
|
||||
resetHeroMoves() {
|
||||
@@ -655,6 +665,18 @@ export class GameEngine {
|
||||
attemptBreakAway(hero) {
|
||||
if (!hero || hero.hasEscapedPin) return { success: false, roll: 0 };
|
||||
|
||||
// RULE: If Derrumbamiento, escape is free
|
||||
if (this.state && this.state.collapsingRoom && this.state.collapsingRoom.tileId) {
|
||||
// Check if hero is in the collapsing room
|
||||
const key = `${Math.floor(hero.x)},${Math.floor(hero.y)}`;
|
||||
const tid = this.dungeon.grid.occupiedCells.get(key);
|
||||
if (tid === this.state.collapsingRoom.tileId) {
|
||||
console.log("[GameEngine] Free BreakAway due to Collapsing Room!");
|
||||
hero.hasEscapedPin = true;
|
||||
return { success: true, roll: "AUTO", target: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
const roll = Math.floor(Math.random() * 6) + 1;
|
||||
const target = hero.stats.pin_target || 6;
|
||||
|
||||
@@ -772,35 +794,31 @@ export class GameEngine {
|
||||
if (isRoom) {
|
||||
console.log(`[GameEngine] Hero entered NEW ROOM: ${tileId}`);
|
||||
|
||||
// Disparar Evento (need cells)
|
||||
const newCells = [];
|
||||
for (const [key, tid] of this.dungeon.grid.occupiedCells.entries()) {
|
||||
if (tid === tileId) {
|
||||
const [cx, cy] = key.split(',').map(Number);
|
||||
newCells.push({ x: cx, y: cy });
|
||||
}
|
||||
triggeredEvents = true; // Stop movement forces end of hero action
|
||||
entity.currentMoves = 0;
|
||||
|
||||
// Send PARTIAL path to renderer (from 0 to current step i+1)
|
||||
if (this.onEntityMove) {
|
||||
this.onEntityMove(entity, fullPath.slice(0, i + 1));
|
||||
}
|
||||
|
||||
// Call Event Logic
|
||||
const eventResult = this.onRoomRevealed(newCells);
|
||||
if (this.onShowMessage) this.onShowMessage("¡Estancia Revelada!", "Explorando...", 2000);
|
||||
|
||||
// Always stop for Rooms
|
||||
if (eventResult) {
|
||||
console.log("Movement stopped by Room Entry!");
|
||||
triggeredEvents = true;
|
||||
// IMMEDIATE EVENT RESOLUTION
|
||||
this.handlePowerEvent({ tileId: tileId }, () => {
|
||||
console.log("[GameEngine] Room Event Resolved.");
|
||||
|
||||
// Notify UI via callback
|
||||
if (this.onEventTriggered) {
|
||||
this.onEventTriggered(eventResult);
|
||||
// Check for Monsters
|
||||
const hasMonsters = this.monsters.some(m => !m.isDead);
|
||||
if (hasMonsters) {
|
||||
console.log("[GameEngine] Monsters Spawned! Ending Hero Phase.");
|
||||
this.turnManager.setPhase('monster');
|
||||
} else {
|
||||
console.log("[GameEngine] No Monsters. Staying in Hero Phase.");
|
||||
}
|
||||
});
|
||||
|
||||
// Send PARTIAL path to renderer (from 0 to current step i+1)
|
||||
if (this.onEntityMove) {
|
||||
this.onEntityMove(entity, fullPath.slice(0, i + 1));
|
||||
}
|
||||
|
||||
break; // Stop loop
|
||||
}
|
||||
break; // Stop loop
|
||||
} else {
|
||||
console.log(`[GameEngine] Hero entered Corridor: ${tileId} (No Stop)`);
|
||||
}
|
||||
@@ -872,14 +890,30 @@ export class GameEngine {
|
||||
// Minimal update loop
|
||||
}
|
||||
|
||||
findSpawnPoints(count) {
|
||||
findSpawnPoints(count, tileId = null) {
|
||||
// Collect all currently available cells (occupiedCells maps "x,y" => tileId)
|
||||
// At the start of the game, this typically contains only the cells of the starting room.
|
||||
// If tileId is provided, filter ONLY cells belonging to that tile.
|
||||
const candidates = [];
|
||||
|
||||
for (const key of this.dungeon.grid.occupiedCells.keys()) {
|
||||
for (const [key, tid] of this.dungeon.grid.occupiedCells.entries()) {
|
||||
if (tileId && tid !== tileId) continue;
|
||||
|
||||
const [x, y] = key.split(',').map(Number);
|
||||
candidates.push({ x, y });
|
||||
|
||||
// Check Collision: Do not spawn on Heroes or existing Monsters
|
||||
const isHero = this.heroes.some(h => h.x === x && h.y === y);
|
||||
const isMonster = this.monsters.some(m => m.x === x && m.y === y && !m.isDead);
|
||||
|
||||
if (!isHero && !isMonster) {
|
||||
candidates.push({ x, y });
|
||||
}
|
||||
}
|
||||
|
||||
// If localized spawn fails (full room), maybe allow spill over to neighbors? (Rules say: "adjacent board sections")
|
||||
if (tileId && candidates.length < count) {
|
||||
console.warn(`[GameEngine] Room ${tileId} full? Searching neighbors...`);
|
||||
// NOTE: Neighbor search logic is complex, skipping for MVP.
|
||||
// Fallback to global search if desperate?
|
||||
}
|
||||
|
||||
// 2. Shuffle candidates (Fisher-Yates) to ensure random but valid placement
|
||||
@@ -888,10 +922,22 @@ export class GameEngine {
|
||||
[candidates[i], candidates[j]] = [candidates[j], candidates[i]];
|
||||
}
|
||||
|
||||
// 3. Return requested amount
|
||||
// 3. Return requested amount (or less if not enough space)
|
||||
if (candidates.length < count) {
|
||||
console.warn(`[GameEngine] Not enough space to spawn ${count} monsters. Only ${candidates.length} spawn.`);
|
||||
if (this.onShowMessage) {
|
||||
// this.onShowMessage("Espacio Insuficiente", `Solo caben ${candidates.length} de ${count} monstruos.`);
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
return candidates.slice(0, count);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
onRoomRevealed(cells) {
|
||||
console.log("[GameEngine] Room Revealed!");
|
||||
|
||||
@@ -1287,71 +1333,221 @@ export class GameEngine {
|
||||
return { clear: !blocked, path, blocker };
|
||||
}
|
||||
|
||||
handlePowerEvent() {
|
||||
|
||||
handlePowerEvent(context = null, onComplete = null) {
|
||||
console.log("[GameEngine] Handling Power Event...");
|
||||
|
||||
// Inject Context
|
||||
if (context) {
|
||||
this.currentEventContext = context;
|
||||
}
|
||||
|
||||
if (!this.eventDeck || this.eventDeck.length === 0) {
|
||||
console.log("[GameEngine] Reshuffling Deck...");
|
||||
this.eventDeck = createEventDeck();
|
||||
}
|
||||
const card = this.eventDeck.shift();
|
||||
let card = this.eventDeck.shift();
|
||||
|
||||
console.log(`[Event] Drawn: ${card.name} (${card.type})`);
|
||||
// RULE: Ignore Derrumbamiento in Start Room
|
||||
// Check if we are in turn 1 of setup or if heroes are in the first tile (0,0)
|
||||
// Or simpler: Check if card is 'evt_derrumbamiento' and currentTileId is 'tile_0'
|
||||
|
||||
if (this.onShowMessage) {
|
||||
// Use specific prefix for Log Routing (if implemented) or just generic
|
||||
this.onShowMessage(`Evento: ${card.name}`, card.description);
|
||||
if (card.id === 'evt_derrumbamiento') {
|
||||
const leader = this.heroes[0];
|
||||
const currentTileId = this.dungeon.grid.occupiedCells.get(`${leader.x},${leader.y}`);
|
||||
|
||||
// Assuming 'tile_0' is the start room ID.
|
||||
// Also checking turn number might be safe: turnManager.currentTurn
|
||||
if (currentTileId === 'tile_0' || this.turnManager.currentTurn <= 1) {
|
||||
console.log("[GameEngine] Derrumbamiento drawn in Start Room. IGNORING and Redrawing.");
|
||||
// Discard and Draw again
|
||||
// (Maybe put back in deck? Rules say 'ignore and draw another immediately', usually means discard this one)
|
||||
if (this.eventDeck.length === 0) this.eventDeck = createEventDeck();
|
||||
card = this.eventDeck.shift();
|
||||
console.log(`[GameEngine] Redrawn Card: ${card.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (card.type === EVENT_TYPES.MONSTER) {
|
||||
let count = 1;
|
||||
const title = card.name.toUpperCase();
|
||||
console.log(`[GameEngine] Drawn Card: ${card.name}`, card);
|
||||
|
||||
if (title.includes('2D6')) {
|
||||
count = Math.floor(Math.random() * 6) + 1 + Math.floor(Math.random() * 6) + 1;
|
||||
} else if (title.includes('1D6')) {
|
||||
count = Math.floor(Math.random() * 6) + 1;
|
||||
} else if (card.data.mecanica && card.data.mecanica.cantidad_inicial) {
|
||||
count = card.data.mecanica.cantidad_inicial;
|
||||
// Delegate execution to the modular interpreter
|
||||
if (this.events) {
|
||||
this.events.processEvent(card, () => {
|
||||
this.currentEventContext = null;
|
||||
if (onComplete) onComplete();
|
||||
else this.turnManager.resumeFromEvent();
|
||||
});
|
||||
} else {
|
||||
console.error("[GameEngine] EventInterpreter not initialized!");
|
||||
this.turnManager.resumeFromEvent();
|
||||
}
|
||||
}
|
||||
|
||||
handleEndTurn() {
|
||||
console.log("[GameEngine] Handling End of Turn Effects...");
|
||||
|
||||
// Check Collapsing Room State
|
||||
if (this.state && this.state.collapsingRoom) {
|
||||
if (typeof this.state.collapsingRoom.turnsLeft === 'number') {
|
||||
this.state.collapsingRoom.turnsLeft--;
|
||||
console.log(`[GameEngine] Collapsing Room Timer: ${this.state.collapsingRoom.turnsLeft}`);
|
||||
|
||||
if (this.state.collapsingRoom.turnsLeft > 0) {
|
||||
if (this.onShowMessage) this.onShowMessage("¡PELIGRO!", `El techo cruje... ¡Queda ${this.state.collapsingRoom.turnsLeft} turno para salir!`, 4000);
|
||||
} else {
|
||||
// TIME'S UP - KILL EVERYONE IN ROOM
|
||||
this.killEntitiesInCollapsingRoom(this.state.collapsingRoom.tileId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Map stats
|
||||
const rawStats = card.data.estadisticas || {};
|
||||
const mappedStats = {
|
||||
move: rawStats.movimiento || 4,
|
||||
ws: rawStats.habilidad_armas || 3,
|
||||
bs: 0,
|
||||
str: rawStats.fuerza === 'Especial' ? 3 : (rawStats.fuerza === 4 ? 4 : 3), // Simplification
|
||||
toughness: rawStats.resistencia || 3,
|
||||
wounds: rawStats.heridas || 1,
|
||||
attacks: rawStats.ataques || 1,
|
||||
gold: card.data.valor_oro || 0
|
||||
};
|
||||
if (!this.heroes) return;
|
||||
|
||||
// Fix Strength mapping if int
|
||||
if (typeof rawStats.fuerza === 'number') mappedStats.str = rawStats.fuerza;
|
||||
this.heroes.forEach(hero => {
|
||||
if (hero.buffs && hero.buffs.length > 0) {
|
||||
// Decrement duration
|
||||
hero.buffs.forEach(buff => {
|
||||
// ... (existing buff logic if any)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Map Portrait
|
||||
let portraitStr = 'assets/images/monsters/orc_portrait.png';
|
||||
if (title.includes('MINOTAURO')) portraitStr = 'assets/images/monsters/minotaur_portrait.png'; // If we had it
|
||||
if (title.includes('ARAÑAS')) portraitStr = 'assets/images/monsters/spider_portrait.png';
|
||||
killEntitiesInCollapsingRoom(tileId) {
|
||||
if (this.onShowMessage) this.onShowMessage("¡DERRUMBE TOTAL!", "La sala se ha venido abajo. Todo lo que había dentro ha sido aplastado.", 5000);
|
||||
|
||||
const def = {
|
||||
name: card.name,
|
||||
portrait: portraitStr,
|
||||
stats: mappedStats
|
||||
};
|
||||
// Find entities in this tile
|
||||
const entitiesToKill = [];
|
||||
|
||||
const spots = this.findSpawnPoints(count);
|
||||
spots.forEach(spot => {
|
||||
this.spawnMonster(def, spot.x, spot.y, { skipTurn: false });
|
||||
// Heroes
|
||||
this.heroes.forEach(h => {
|
||||
const key = `${Math.floor(h.x)},${Math.floor(h.y)}`;
|
||||
const tid = this.dungeon.grid.occupiedCells.get(key);
|
||||
if (tid === tileId) entitiesToKill.push(h);
|
||||
});
|
||||
|
||||
// Monsters
|
||||
this.monsters.forEach(m => {
|
||||
if (m.isDead) return;
|
||||
const key = `${Math.floor(m.x)},${Math.floor(m.y)}`;
|
||||
const tid = this.dungeon.grid.occupiedCells.get(key);
|
||||
if (tid === tileId) entitiesToKill.push(m);
|
||||
});
|
||||
|
||||
entitiesToKill.forEach(e => {
|
||||
console.log(`[GameEngine] Crushed by Rockfall: ${e.name}`);
|
||||
// Instant Kill logic
|
||||
if (e.currentWounds) e.currentWounds = 0;
|
||||
e.isDead = true;
|
||||
if (this.onEntityDeath) this.onEntityDeath(e.id);
|
||||
});
|
||||
|
||||
// Clear state so it doesn't kill again (optional, room is gone)
|
||||
this.state.collapsingRoom = null;
|
||||
}
|
||||
collapseExits() {
|
||||
// Logic: Find the room the heroes are in, identify its UNOPENED exits (which are in availableExits), and remove them.
|
||||
|
||||
// 1. Find Current Room ID based on Leader
|
||||
const leader = this.heroes[0]; // Assume leader is first
|
||||
if (!leader) return 0;
|
||||
|
||||
const currentTileId = this.dungeon.grid.occupiedCells.get(`${leader.x},${leader.y}`);
|
||||
if (!currentTileId) {
|
||||
console.warn("CollapseExits: Leader not on a valid tile.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
console.log(`[GameEngine] Collapse Triggered in ${currentTileId}`);
|
||||
|
||||
// Start Countdown State
|
||||
if (!this.state) this.state = {};
|
||||
this.state.collapsingRoom = {
|
||||
tileId: currentTileId,
|
||||
turnsLeft: 1 // Next monster phase ends turn -> Next power phase -> Next monster phase = Death
|
||||
};
|
||||
|
||||
// 2. Scan Available Exits to see which align with this Room
|
||||
// An exit is "part" of a room if it is adjacent to any cell of that room.
|
||||
// Or simpler: The DungeonGenerator keeps track of exits. We scan them.
|
||||
|
||||
// We need to identify cells belonging to currentTileId first.
|
||||
const roomCells = [];
|
||||
for (const [key, tid] of this.dungeon.grid.occupiedCells.entries()) {
|
||||
if (tid === currentTileId) {
|
||||
const [x, y] = key.split(',').map(Number);
|
||||
roomCells.push({ x, y });
|
||||
}
|
||||
}
|
||||
|
||||
const exitsToRemove = [];
|
||||
const blockedLocations = new Set();
|
||||
|
||||
this.dungeon.availableExits.forEach((exit, index) => {
|
||||
// Check adjacency to any room cell
|
||||
const isAdjacent = roomCells.some(cell => {
|
||||
const dx = Math.abs(cell.x - exit.x);
|
||||
const dy = Math.abs(cell.y - exit.y);
|
||||
return (dx + dy === 1);
|
||||
});
|
||||
|
||||
if (this.onEventTriggered) {
|
||||
this.onEventTriggered({ type: 'MONSTER_SPAWN', count: spots.length, message: card.description });
|
||||
if (isAdjacent) {
|
||||
exitsToRemove.push(index);
|
||||
|
||||
this.placeEventMarker("escombros", exit.x, exit.y);
|
||||
blockedLocations.add(`${exit.x},${exit.y}`);
|
||||
|
||||
// Also block the door visually in Renderer
|
||||
if (window.RENDERER) {
|
||||
window.RENDERER.blockDoor({ x: exit.x, y: exit.y });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Remove them (iterate backwards to avoid index shuffle issues)
|
||||
exitsToRemove.sort((a, b) => b - a);
|
||||
exitsToRemove.forEach(idx => {
|
||||
this.dungeon.availableExits.splice(idx, 1);
|
||||
});
|
||||
|
||||
// 4. Calculate Logical "Exits" Count (Approximation)
|
||||
let visualDoorCount = 0;
|
||||
const processedLocs = new Set();
|
||||
|
||||
for (const loc of blockedLocations) {
|
||||
if (processedLocs.has(loc)) continue;
|
||||
visualDoorCount++;
|
||||
processedLocs.add(loc);
|
||||
|
||||
const [x, y] = loc.split(',').map(Number);
|
||||
const neighbors = [`${x + 1},${y}`, `${x - 1},${y}`, `${x},${y + 1}`, `${x},${y - 1}`];
|
||||
neighbors.forEach(n => {
|
||||
if (blockedLocations.has(n)) processedLocs.add(n);
|
||||
});
|
||||
}
|
||||
|
||||
// Generic delay to resume
|
||||
setTimeout(() => {
|
||||
if (this.turnManager) this.turnManager.nextPhase();
|
||||
}, 3000);
|
||||
console.log(`[GameEngine] Collapsed ${exitsToRemove.length} exit cells (${visualDoorCount} visual doors).`);
|
||||
return visualDoorCount;
|
||||
}
|
||||
|
||||
placeEventMarker(type, specificX = null, specificY = null) {
|
||||
// Place a visual marker in the world
|
||||
// If x,y not provided, place in center of heroes
|
||||
let x = specificX;
|
||||
let y = specificY;
|
||||
|
||||
if (x === null || y === null) {
|
||||
// Center on leader
|
||||
const leader = this.heroes[0];
|
||||
x = leader.x;
|
||||
y = leader.y;
|
||||
}
|
||||
|
||||
console.log(`[GameEngine] Placing Marker '${type}' at ${x},${y}`);
|
||||
|
||||
// Delegate to Renderer
|
||||
if (window.RENDERER) {
|
||||
window.RENDERER.spawnProp(type, x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,19 +55,24 @@ export class TurnManager {
|
||||
this.rollPowerDice();
|
||||
}
|
||||
|
||||
resumeFromEvent() {
|
||||
console.log("Resuming from Event...");
|
||||
this.nextPhase();
|
||||
}
|
||||
|
||||
rollPowerDice() {
|
||||
const roll = Math.floor(Math.random() * 6) + 1;
|
||||
// const roll = 1; // DEBUG: Force Event for testing
|
||||
this.currentPowerRoll = roll;
|
||||
console.log(`Power Roll: ${roll}`);
|
||||
|
||||
let message = "El poder fluye...";
|
||||
let eventTriggered = false;
|
||||
|
||||
// Placeholder for future Event Logic
|
||||
if (roll === 1) {
|
||||
message = "¡EVENTO DE PODER! (1) (Bypass Temporal)";
|
||||
eventTriggered = false; // Bypass for now to prevent freeze
|
||||
// eventTriggered = true;
|
||||
// logic delegated to listeners
|
||||
message = "¡EVENTO DE PODER! (1)";
|
||||
eventTriggered = true;
|
||||
}
|
||||
|
||||
this.emit('POWER_RESULT', { roll, message, eventTriggered });
|
||||
|
||||
35
src/main.js
@@ -110,19 +110,18 @@ game.onCombatResult = (log) => {
|
||||
const atkName = attacker ? attacker.name : '???';
|
||||
const defName = defender ? defender.name : '???';
|
||||
|
||||
let logMsg = `<b>${atkName}</b> ataca a <b>${defName}</b>.<br>`;
|
||||
// 1. Format Log Message
|
||||
// Use the comprehensive message generated by CombatMechanics
|
||||
let logMsg = log.message;
|
||||
|
||||
// Determine type for color
|
||||
let type = 'combat-miss';
|
||||
if (log.hitSuccess) {
|
||||
logMsg += `¡Impacto! (Dado: ${log.hitRoll}). `;
|
||||
if (log.woundsCaused > 0) {
|
||||
logMsg += `Causa <b>${log.woundsCaused}</b> heridas.`;
|
||||
} else {
|
||||
logMsg += `Armadura absorbe el daño.`;
|
||||
}
|
||||
} else {
|
||||
logMsg += `Falla (Dado: ${log.hitRoll}).`;
|
||||
type = log.woundsCaused > 0 ? 'combat-hit' : 'combat-miss'; // Or create 'combat-block' style
|
||||
if (log.defenderDied) type = 'combat-kill';
|
||||
}
|
||||
|
||||
ui.addLog(logMsg, log.hitSuccess ? 'combat-hit' : 'combat-miss');
|
||||
ui.addLog(logMsg, type);
|
||||
|
||||
// 2. Show Attack Roll on Attacker (Floating)
|
||||
if (attacker) {
|
||||
@@ -190,14 +189,26 @@ game.onRangedTarget = (targetMonster, losResult) => {
|
||||
|
||||
game.onShowMessage = (title, message, duration) => {
|
||||
// Filter specific game flow messages to Log instead of popup
|
||||
if (title.startsWith('Turno de') || title.includes('Fase') || title.includes('Efecto')) {
|
||||
ui.addLog(`👉 <b>${title}</b>: ${message}`, 'system');
|
||||
if (title.startsWith('Turno de') || title.includes('Fase') || title.includes('Efecto') || title.includes('Evento')) {
|
||||
let icon = '👉';
|
||||
let type = 'system';
|
||||
|
||||
if (title.includes('Evento')) {
|
||||
icon = '⚡';
|
||||
type = 'event-log';
|
||||
}
|
||||
|
||||
ui.addLog(`${icon} <b>${title}</b>: ${message}`, type);
|
||||
return;
|
||||
}
|
||||
// Default fallback for other messages (e.g. Warnings not covered by floating text)
|
||||
ui.showTemporaryMessage(title, message, duration);
|
||||
};
|
||||
|
||||
game.onShowEvent = (cardData, callback) => {
|
||||
ui.showEventCard(cardData, callback);
|
||||
};
|
||||
|
||||
// game.onEntitySelect is now handled by UIManager to wrap the renderer call
|
||||
|
||||
renderer.onHeroFinishedMove = (x, y) => {
|
||||
|
||||
@@ -141,6 +141,13 @@ export class GameRenderer {
|
||||
this.entityRenderer.triggerDeathAnimation(entityId);
|
||||
}
|
||||
|
||||
spawnProp(type, x, y) {
|
||||
// Delegate to DungeonRenderer as it handles static props
|
||||
// Or if it's an effect, EffectsRenderer.
|
||||
// For "escombros" marker, it's a static prop.
|
||||
this.dungeonRenderer.spawnProp(type, x, y);
|
||||
}
|
||||
|
||||
triggerVisualEffect(type, x, y) {
|
||||
this.effectsRenderer.triggerVisualEffect(type, x, y);
|
||||
}
|
||||
|
||||
@@ -113,6 +113,7 @@ export class UIManager {
|
||||
showTemporaryMessage(t, m, d) { this.feedback.showTemporaryMessage(t, m, d); }
|
||||
showCombatLog(log) { this.feedback.addLogMessage(log.message, log.hitSuccess ? 'combat-hit' : 'combat-miss'); }
|
||||
addLog(message, type) { this.feedback.addLogMessage(message, type); }
|
||||
showEventCard(cardData, callback) { this.feedback.showEventCard(cardData, callback); }
|
||||
showRangedAttackUI(monster) { this.cards.showRangedAttackUI(monster); }
|
||||
hideMonsterCard() { this.cards.hideMonsterCard(); }
|
||||
}
|
||||
|
||||
@@ -364,4 +364,28 @@ export class DungeonRenderer {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
spawnProp(type, x, y) {
|
||||
// Simple Prop System for Events
|
||||
const textureMap = {
|
||||
'escombros': '/assets/images/dungeon1/props/debris.png', // Fallback needed?
|
||||
'piedras': '/assets/images/dungeon1/props/rocks.png'
|
||||
};
|
||||
|
||||
// Fallback for missing assets: reuse known specific textures or generic
|
||||
let path = textureMap[type] || '/assets/images/dungeon1/doors/door1_blocked.png'; // Use blocked door as generic debris for now if others missing
|
||||
|
||||
this.getTexture(path, (texture) => {
|
||||
const mat = new THREE.SpriteMaterial({ map: texture });
|
||||
const sprite = new THREE.Sprite(mat);
|
||||
|
||||
// Positioning
|
||||
sprite.position.set(x, 0.5, -y);
|
||||
sprite.scale.set(1, 1, 1);
|
||||
|
||||
this.dungeonGroup.add(sprite);
|
||||
|
||||
// Add simple logic to remove it later if needed? For now permanent.
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ export class FeedbackUI {
|
||||
if (type === 'success') entry.style.color = '#66ff66';
|
||||
if (type === 'warning') entry.style.color = '#ffcc00';
|
||||
if (type === 'system') entry.style.color = '#88ccff';
|
||||
if (type === 'event-log') entry.style.color = '#44ff44'; // Bright Green
|
||||
|
||||
entry.innerHTML = message;
|
||||
|
||||
@@ -169,6 +170,109 @@ export class FeedbackUI {
|
||||
this.parentContainer.appendChild(overlay);
|
||||
}
|
||||
|
||||
showEventCard(cardData, callback) {
|
||||
const overlay = document.createElement('div');
|
||||
Object.assign(overlay.style, {
|
||||
position: 'absolute', top: '0', left: '0', width: '100%', height: '100%',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.85)', display: 'flex', justifyContent: 'center', alignItems: 'center',
|
||||
pointerEvents: 'auto', zIndex: '2000'
|
||||
});
|
||||
|
||||
// Card Container
|
||||
const card = document.createElement('div');
|
||||
Object.assign(card.style, {
|
||||
backgroundColor: '#1a1a1a',
|
||||
backgroundImage: 'repeating-linear-gradient(45deg, #222 25%, transparent 25%, transparent 75%, #222 75%, #222), repeating-linear-gradient(45deg, #222 25%, #1a1a1a 25%, #1a1a1a 75%, #222 75%, #222)',
|
||||
backgroundPosition: '0 0, 10px 10px',
|
||||
backgroundSize: '20px 20px',
|
||||
border: '4px solid #8b0000',
|
||||
borderRadius: '12px',
|
||||
padding: '30px',
|
||||
width: '320px',
|
||||
textAlign: 'center',
|
||||
color: '#fff',
|
||||
fontFamily: '"Cinzel", serif',
|
||||
boxShadow: '0 0 30px rgba(139, 0, 0, 0.6), inset 0 0 50px rgba(0,0,0,0.8)',
|
||||
position: 'relative'
|
||||
});
|
||||
|
||||
// Title
|
||||
const titleEl = document.createElement('h2');
|
||||
titleEl.textContent = cardData.titulo || "Evento";
|
||||
Object.assign(titleEl.style, {
|
||||
marginTop: '0',
|
||||
marginBottom: '10px',
|
||||
color: '#ff4444',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '2px',
|
||||
fontSize: '24px',
|
||||
textShadow: '2px 2px 0 #000'
|
||||
});
|
||||
card.appendChild(titleEl);
|
||||
|
||||
// Subtitle/Type
|
||||
if (cardData.tipo) {
|
||||
const typeEl = document.createElement('div');
|
||||
typeEl.textContent = cardData.tipo;
|
||||
Object.assign(typeEl.style, {
|
||||
fontSize: '12px',
|
||||
color: '#aaa',
|
||||
marginBottom: '20px',
|
||||
textTransform: 'uppercase',
|
||||
borderBottom: '1px solid #444',
|
||||
paddingBottom: '5px'
|
||||
});
|
||||
card.appendChild(typeEl);
|
||||
}
|
||||
|
||||
// Image Placeholder (Optional)
|
||||
// const img = document.createElement('div'); ...
|
||||
|
||||
// Message
|
||||
const msgEl = document.createElement('p');
|
||||
// If it's pure text or HTML
|
||||
msgEl.innerHTML = cardData.descripcion || cardData.texto || "";
|
||||
Object.assign(msgEl.style, {
|
||||
fontSize: '16px',
|
||||
lineHeight: '1.6',
|
||||
color: '#ddd',
|
||||
textAlign: 'justify',
|
||||
fontStyle: 'italic',
|
||||
marginBottom: '25px'
|
||||
});
|
||||
card.appendChild(msgEl);
|
||||
|
||||
// Action Button
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = 'CONTINUAR';
|
||||
Object.assign(btn.style, {
|
||||
padding: '12px 30px',
|
||||
fontSize: '16px',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: '#8b0000',
|
||||
color: '#fff',
|
||||
border: '2px solid #ff4444',
|
||||
borderRadius: '4px',
|
||||
fontFamily: 'inherit',
|
||||
fontWeight: 'bold',
|
||||
textTransform: 'uppercase',
|
||||
boxShadow: '0 4px 0 #440000',
|
||||
transition: 'transform 0.1s'
|
||||
});
|
||||
|
||||
btn.onmousedown = () => btn.style.transform = 'translateY(2px)';
|
||||
btn.onmouseup = () => btn.style.transform = 'translateY(0)';
|
||||
|
||||
btn.onclick = () => {
|
||||
if (overlay.parentNode) this.parentContainer.removeChild(overlay);
|
||||
if (callback) callback();
|
||||
};
|
||||
card.appendChild(btn);
|
||||
|
||||
overlay.appendChild(card);
|
||||
this.parentContainer.appendChild(overlay);
|
||||
}
|
||||
|
||||
showTemporaryMessage(title, message, duration = 2000) {
|
||||
const modal = document.createElement('div');
|
||||
Object.assign(modal.style, {
|
||||
|
||||