Sesión 15: Implementación de Inventario y lógica de evento de Rastrillo/Llave
This commit is contained in:
30
DEVLOG.md
30
DEVLOG.md
@@ -1,5 +1,35 @@
|
||||
# Devlog - Warhammer Quest (Versión Web 3D)
|
||||
|
||||
## Sesión 15: Evento del Rastrillo, Llave del Enano e Inventario
|
||||
**Fecha:** 10 de Enero de 2026
|
||||
|
||||
### Objetivos
|
||||
- Implementar el evento del "Rastrillo" (bloqueo de entrada por portcullis).
|
||||
- Crear un sistema de inventario funcional para todos los héroes.
|
||||
- Vincular la adquisición de la "Llave del Rastrillo" con la capacidad de abrir la puerta bloqueada.
|
||||
|
||||
### Cambios Realizados
|
||||
|
||||
#### 1. Sistema de Inventario
|
||||
- **Propiedad Universal**: Se ha añadido un array `inventory` a nivel de objeto para todos los héroes en su inicialización.
|
||||
- **Interfaz Visual (`InventoryUI.js`)**: Nueva interfaz estilo "mochila" que muestra los items recolectados por cada héroe.
|
||||
- **Integración en UI**: Habilitado el botón "🎒 INVENTARIO" en las fichas de unidad para desplegar el contenido de la mochila.
|
||||
- **Gestión de Items**: El `EventInterpreter` ahora soporta la acción `EFECTO (tipo: item)`, permitiendo que los eventos entreguen objetos físicos a los aventureros.
|
||||
|
||||
#### 2. Evento del Rastrillo (Portcullis)
|
||||
- **Bloqueo Inteligente**: El `GameEngine` ahora rastrea la última entrada utilizada (`lastEntranceUsed`).
|
||||
- **Nuevas Acciones de Entorno**: Implementado el subtipo `bloquear_entrada_rastrillo` en `EventInterpreter`.
|
||||
- **Detección de Llave**: Al intentar abrir una puerta bloqueada por un rastrillo, el juego verifica si algún héroe del grupo posee la `llave_rastrillo`.
|
||||
- **Textura de Rastrillo**: Se añadió soporte para `door1_portcullis.png` en `DungeonRenderer.js`.
|
||||
- **Safeguards de Renderizado**: Se añadieron protecciones en el renderizador de puertas para evitar que la animación de "abrir puerta" sobreescriba visualmente el estado de "puerta bloqueada" o "portcullis".
|
||||
|
||||
### Estado Actual
|
||||
El sistema de inventario y la lógica de la llave funcionan correctamente. El evento del Enano Moribundo entrega la llave al grupo, y esta permite levantar el rastrillo.
|
||||
**Pendiente**: Se ha detectado un problema visual donde la textura de `door1_portcullis.png` no parece aplicarse correctamente en algunos casos, a pesar de que la lógica de bloqueo funciona. Se requiere una revisión más profunda del sistema de materiales de Three.js para este caso.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Sesión 14: Estabilización del Flujo de Juego y Bugfix Crítico
|
||||
**Fecha:** 10 de Enero de 2026
|
||||
|
||||
|
||||
BIN
public/assets/images/dungeon1/doors/door1_portcullis.png
Normal file
BIN
public/assets/images/dungeon1/doors/door1_portcullis.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 2.3 MiB |
BIN
public/assets/images/dungeon1/standees/enemies/Lordwarlock_1.png
Normal file
BIN
public/assets/images/dungeon1/standees/enemies/Lordwarlock_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 497 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 646 KiB |
@@ -29,7 +29,7 @@ export const EVENT_CARDS_DATA = [
|
||||
"titulo": "CADÁVER",
|
||||
"tipo": "Evento",
|
||||
"codigo_tipo": "E",
|
||||
"descripcion": "Un Bárbaro muerto sostiene una bolsa de cuero.",
|
||||
"descripcion": "Encontráis un cadáver que sostiene una bolsa de cuero. ¿Contendrá algún tesoro o será una trampa?",
|
||||
"cita_fuente": "[cite: 6, 7, 11, 15, 16]",
|
||||
"acciones": [
|
||||
{
|
||||
@@ -37,7 +37,7 @@ export const EVENT_CARDS_DATA = [
|
||||
"modo": "tirada_baja",
|
||||
"dado": "1D6",
|
||||
"guardar_como": "victima",
|
||||
"mensaje": "Cada jugador tira 1D6. El más bajo investiga el cadáver."
|
||||
"mensaje": "se acerca con cautela para investigar el cadáver..."
|
||||
},
|
||||
{
|
||||
"tipo_accion": "PRUEBA",
|
||||
@@ -45,15 +45,13 @@ export const EVENT_CARDS_DATA = [
|
||||
"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." }
|
||||
{ "tipo_accion": "EFECTO", "objetivo": "victima", "tipo": "daño", "cantidad": "1D6", "mensaje": "¡Gas venenoso! 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." }
|
||||
{ "tipo_accion": "EFECTO", "objetivo": "victima", "tipo": "daño", "cantidad": "2D6", "mensaje": "¡Trampa de lanza! La bolsa estaba vacía." }
|
||||
],
|
||||
"4-6": [
|
||||
{ "tipo_accion": "EFECTO", "objetivo": "victima", "tipo": "oro", "cantidad": "1D6*100", "mensaje": "¡Encuentras oro!" }
|
||||
{ "tipo_accion": "EFECTO", "objetivo": "victima", "tipo": "oro", "cantidad": "1D6*100", "mensaje": "¡Encuentras monedas de oro en la bolsa!" }
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -68,32 +66,32 @@ export const EVENT_CARDS_DATA = [
|
||||
"titulo": "VIEJOS HUESOS",
|
||||
"tipo": "Evento",
|
||||
"codigo_tipo": "E",
|
||||
"descripcion": "Suelo cubierto de huesos y cráneos con brillo de monedas bajo ellos.",
|
||||
"descripcion": "El suelo está cubierto de huesos y cráneos blanquecinos. Entre ellos, el brillo inconfundible del oro tienta a vuestra suerte...",
|
||||
"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.",
|
||||
"tipo_prueba": "1D6",
|
||||
"mensaje": "Alguien remueve los huesos con la punta de su bota...",
|
||||
"tabla": {
|
||||
"1": [
|
||||
{ "tipo_accion": "MENSAJE", "texto": "Ilusión. Los huesos y el oro desaparecen." },
|
||||
{ "tipo_accion": "MENSAJE", "texto": "¡Era una ilusión! Los huesos y el brillo del oro se desvanecen en el aire." },
|
||||
{ "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": "SELECCION", "modo": "azar", "guardar_como": "victima", "mensaje": "activan un mecanismo de defensa!" },
|
||||
{ "tipo_accion": "EFECTO", "objetivo": "victima", "tipo": "daño", "cantidad": "1D6", "mensaje": "¡Un rayo mágico brota de una calavera!" },
|
||||
{ "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": "SELECCION", "modo": "todos", "mensaje": "se reparten las monedas encontradas." },
|
||||
{ "tipo_accion": "EFECTO", "tipo": "oro", "cantidad": "1D6*10", "mensaje": "Había algo de oro entre los restos." },
|
||||
{ "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." }
|
||||
{ "tipo_accion": "SELECCION", "modo": "todos", "mensaje": "¡están de suerte!" },
|
||||
{ "tipo_accion": "EFECTO", "tipo": "oro", "cantidad": "2D6*10", "mensaje": "¡Un gran montón de oro estaba oculto bajo los cráneos!" },
|
||||
{ "tipo_accion": "EFECTO", "tipo": "carta_tesoro", "cantidad": 1, "mensaje": "¡Y además encontráis un objeto valioso!" }
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -104,7 +102,7 @@ export const EVENT_CARDS_DATA = [
|
||||
"titulo": "TRAMPA",
|
||||
"tipo": "Evento",
|
||||
"codigo_tipo": "E",
|
||||
"descripcion": "El Aventurero con el resultado menor activa una trampa.",
|
||||
"descripcion": "¡Click! De repente todos oís un ruido metálico bajo vuestros pies... ¡Alguien ha activado una trampa!",
|
||||
"cita_fuente": "[cite: 19, 20, 21, 22, 23, 24]",
|
||||
"acciones": [
|
||||
{
|
||||
@@ -112,7 +110,7 @@ export const EVENT_CARDS_DATA = [
|
||||
"modo": "tirada_baja",
|
||||
"dado": "1D6",
|
||||
"guardar_como": "victima",
|
||||
"mensaje": "¡Click! Alguien ha pisado algo..."
|
||||
"mensaje": "ha pisado una trampa!"
|
||||
},
|
||||
{
|
||||
"tipo_accion": "PRUEBA",
|
||||
@@ -139,6 +137,33 @@ export const EVENT_CARDS_DATA = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "evt_enano_moribundo",
|
||||
"titulo": "ENCUENTRO",
|
||||
"tipo": "Evento",
|
||||
"codigo_tipo": "E",
|
||||
"descripcion": "Al desplomarse una pared, encontráis a un Enano Minero moribundo acribillado por flechas. Antes de morir, os entrega una llave importante.",
|
||||
"cita_fuente": "\"Esta es la llave que abrirá el rastrillo. Sin ella jamás seréis capaces de atravesarlo.\"",
|
||||
"acciones": [
|
||||
{
|
||||
"tipo_accion": "SELECCION",
|
||||
"modo": "azar",
|
||||
"guardar_como": "portador",
|
||||
"mensaje": "se acerca al enano y recibe la llave de sus manos trémulas."
|
||||
},
|
||||
{
|
||||
"tipo_accion": "EFECTO",
|
||||
"objetivo": "portador",
|
||||
"tipo": "item",
|
||||
"id_item": "llave_rastrillo",
|
||||
"mensaje": "¡Has conseguido la Llave del Rastrillo!"
|
||||
},
|
||||
{
|
||||
"tipo_accion": "TRIGGER_EVENTO",
|
||||
"cantidad": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "evt_rastrillo",
|
||||
"titulo": "RASTRILLO",
|
||||
@@ -149,13 +174,7 @@ export const EVENT_CARDS_DATA = [
|
||||
"acciones": [
|
||||
{
|
||||
"tipo_accion": "ENTORNO",
|
||||
"subtipo": "bloquear_salida_entrada"
|
||||
},
|
||||
{
|
||||
"tipo_accion": "ENTORNO",
|
||||
"subtipo": "colocar_marcador",
|
||||
"marcador": "rastrillo",
|
||||
"posicion": "entrada"
|
||||
"subtipo": "bloquear_entrada_rastrillo"
|
||||
},
|
||||
{
|
||||
"tipo_accion": "TRIGGER_EVENTO",
|
||||
@@ -196,10 +215,7 @@ export const EVENT_CARDS_DATA = [
|
||||
"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
|
||||
}
|
||||
"cantidad": "1"
|
||||
},
|
||||
{
|
||||
"tipo_accion": "TRIGGER_EVENTO",
|
||||
@@ -218,10 +234,7 @@ export const EVENT_CARDS_DATA = [
|
||||
"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
|
||||
}
|
||||
"cantidad": "2"
|
||||
},
|
||||
{
|
||||
"tipo_accion": "TRIGGER_EVENTO",
|
||||
@@ -240,11 +253,7 @@ export const EVENT_CARDS_DATA = [
|
||||
"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"]
|
||||
"cantidad": "2D6"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -17,13 +17,28 @@ const shuffleDeck = (deck) => {
|
||||
[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];
|
||||
// DEBUG: Force ENANO and RASTRILLO to TOP
|
||||
const enanoIdx = deck.findIndex(c => c.id === 'evt_enano_moribundo');
|
||||
const rastrilloIdx = deck.findIndex(c => c.id === 'evt_rastrillo');
|
||||
|
||||
// Reverse order for unshift
|
||||
if (rastrilloIdx !== -1) {
|
||||
const card = deck.splice(rastrilloIdx, 1)[0];
|
||||
deck.unshift(card);
|
||||
console.log("DEBUG: Forced DERRUMBAMIENTO to top of deck.");
|
||||
}
|
||||
if (enanoIdx !== -1) {
|
||||
const card = deck.splice(enanoIdx, 1)[0];
|
||||
deck.unshift(card);
|
||||
console.log("DEBUG: Forced ENANO MORIBUNDO and RASTRILLO to top of deck.");
|
||||
}
|
||||
|
||||
// DEBUG: Force Chaos Warrior to TOP
|
||||
/*
|
||||
const cwIdx = deck.findIndex(c => c.id === 'mon_chaosWarrior');
|
||||
if (cwIdx !== -1) {
|
||||
const card = deck.splice(cwIdx, 1)[0];
|
||||
deck.unshift(card);
|
||||
console.log("DEBUG: Forced CHAOS WARRIOR to top of deck.");
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
@@ -6,13 +6,14 @@ export const HERO_DEFINITIONS = {
|
||||
stats: {
|
||||
move: 4,
|
||||
ws: 3,
|
||||
to_hit_missile: 5, // 5+ to hit with ranged
|
||||
to_hit_missile: 5,
|
||||
str: 4,
|
||||
toughness: 4, // 3 Base + 1 Armor (Pieles Gruesas)
|
||||
wounds: 12, // 1D6 + 9 (Using fixed average for now)
|
||||
toughness: 4,
|
||||
wounds: 12,
|
||||
attacks: 1,
|
||||
init: 3,
|
||||
pin_target: 6 // 6+ to escape pin
|
||||
pin_target: 6,
|
||||
gold: 0
|
||||
}
|
||||
},
|
||||
dwarf: {
|
||||
@@ -22,13 +23,14 @@ export const HERO_DEFINITIONS = {
|
||||
stats: {
|
||||
move: 4,
|
||||
ws: 4,
|
||||
to_hit_missile: 5, // 5+ to hit with ranged
|
||||
to_hit_missile: 5,
|
||||
str: 3,
|
||||
toughness: 5, // 4 Base + 1 Armor (Cota de Malla)
|
||||
wounds: 11, // 1D6 + 8 (Using fixed average for now)
|
||||
toughness: 5,
|
||||
wounds: 11,
|
||||
attacks: 1,
|
||||
init: 2,
|
||||
pin_target: 5 // 5+ to escape pin
|
||||
pin_target: 5,
|
||||
gold: 0
|
||||
}
|
||||
},
|
||||
elf: {
|
||||
@@ -38,14 +40,15 @@ export const HERO_DEFINITIONS = {
|
||||
stats: {
|
||||
move: 4,
|
||||
ws: 4,
|
||||
bs: 4, // Added for Bow
|
||||
to_hit_missile: 4, // 4+ to hit with ranged
|
||||
bs: 4,
|
||||
to_hit_missile: 4,
|
||||
str: 3,
|
||||
toughness: 3,
|
||||
wounds: 10, // 1D6 + 7 (Using fixed average for now)
|
||||
wounds: 10,
|
||||
attacks: 1,
|
||||
init: 6,
|
||||
pin_target: 1 // Auto escape ("No se puede trabar al Elfo")
|
||||
pin_target: 1,
|
||||
gold: 0
|
||||
}
|
||||
},
|
||||
wizard: {
|
||||
@@ -55,14 +58,15 @@ export const HERO_DEFINITIONS = {
|
||||
stats: {
|
||||
move: 4,
|
||||
ws: 2,
|
||||
to_hit_missile: 6, // 6+ to hit with ranged
|
||||
to_hit_missile: 6,
|
||||
str: 3,
|
||||
toughness: 3,
|
||||
wounds: 9, // 1D6 + 6 (Using fixed average for now)
|
||||
wounds: 9,
|
||||
attacks: 1,
|
||||
init: 3,
|
||||
power: 0, // Tracks current power points
|
||||
pin_target: 4 // 4+ to escape pin
|
||||
power: 0,
|
||||
pin_target: 4,
|
||||
gold: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ export const MONSTER_DEFINITIONS = {
|
||||
gold: 55 // Card: "Valor 55x Unidad"
|
||||
}
|
||||
},
|
||||
// Fix duplicate wounds key in goblin
|
||||
goblin_spearman: {
|
||||
id: 'goblin_spearman',
|
||||
name: 'Lancero Goblin',
|
||||
@@ -22,7 +23,6 @@ export const MONSTER_DEFINITIONS = {
|
||||
ws: 2,
|
||||
str: 3,
|
||||
toughness: 3,
|
||||
wounds: 3,
|
||||
wounds: 1,
|
||||
attacks: 1,
|
||||
gold: 20,
|
||||
@@ -47,7 +47,7 @@ export const MONSTER_DEFINITIONS = {
|
||||
giant_spider: {
|
||||
id: 'giant_spider',
|
||||
name: 'Araña Gigante',
|
||||
portrait: '/assets/images/dungeon1/standees/enemies/spider.png',
|
||||
portrait: '/assets/images/dungeon1/standees/enemies/spiderGiant.png',
|
||||
stats: {
|
||||
move: 6,
|
||||
ws: 2,
|
||||
@@ -88,5 +88,33 @@ export const MONSTER_DEFINITIONS = {
|
||||
gold: 440,
|
||||
damageDice: 2 // "Tira 2 dados para herir"
|
||||
}
|
||||
},
|
||||
chaos_warrior: {
|
||||
id: 'chaos_warrior',
|
||||
name: 'Guerrero del Caos',
|
||||
portrait: '/assets/images/dungeon1/standees/enemies/chaosWarrior.png',
|
||||
stats: {
|
||||
move: 4,
|
||||
ws: 4,
|
||||
str: 4,
|
||||
toughness: 4,
|
||||
wounds: 10, // Copied from Event Card logic
|
||||
attacks: 2,
|
||||
gold: 240
|
||||
}
|
||||
},
|
||||
skaven: {
|
||||
id: 'skaven',
|
||||
name: 'Skaven',
|
||||
portrait: '/assets/images/dungeon1/standees/enemies/skaven.png',
|
||||
stats: {
|
||||
move: 5,
|
||||
ws: 3,
|
||||
str: 3,
|
||||
toughness: 3,
|
||||
wounds: 1,
|
||||
attacks: 1,
|
||||
gold: 30
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
/**
|
||||
* EventInterpreter.js
|
||||
*
|
||||
* Takes high-level Action Instructions from Event Cards (JSON)
|
||||
* and executes them using the Game Engine's low-level systems.
|
||||
*/
|
||||
import { MONSTER_DEFINITIONS } from '../data/Monsters.js';
|
||||
import { CombatMechanics } from '../game/CombatMechanics.js';
|
||||
|
||||
export class EventInterpreter {
|
||||
constructor(gameEngine) {
|
||||
@@ -72,24 +68,19 @@ export class EventInterpreter {
|
||||
});
|
||||
}
|
||||
|
||||
async log(title, text) {
|
||||
if (this.game.onShowMessage) {
|
||||
this.game.onShowMessage(title, text);
|
||||
}
|
||||
// Delay removed here, pacing is now handled only by the queue
|
||||
return Promise.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 {
|
||||
@@ -109,9 +100,9 @@ export class EventInterpreter {
|
||||
|
||||
this.isProcessing = false;
|
||||
|
||||
// Next step
|
||||
// Increased delay to 2s between major event steps as requested
|
||||
if (this.queue.length > 0) {
|
||||
setTimeout(() => this.processQueue(onComplete), 500);
|
||||
setTimeout(() => this.processQueue(onComplete), 2000);
|
||||
} else {
|
||||
this.processQueue(onComplete); // Finish up
|
||||
}
|
||||
@@ -120,7 +111,7 @@ export class EventInterpreter {
|
||||
async executeAction(action) {
|
||||
switch (action.tipo_accion) {
|
||||
case 'MENSAJE':
|
||||
await this.showModal("Evento", action.texto || action.mensaje);
|
||||
await this.log("Evento", action.texto || action.mensaje);
|
||||
break;
|
||||
|
||||
case 'SELECCION':
|
||||
@@ -167,14 +158,11 @@ export class EventInterpreter {
|
||||
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];
|
||||
@@ -182,24 +170,22 @@ export class EventInterpreter {
|
||||
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
|
||||
this.currentContext[action.guardar_como] = targets[0];
|
||||
}
|
||||
|
||||
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>`);
|
||||
// Improved immersive message: "¡El Bárbaro ha pisado una trampa!"
|
||||
await this.log("Selección", `¡El <b>${names}</b> ${action.mensaje}`);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -207,21 +193,19 @@ export class EventInterpreter {
|
||||
}
|
||||
|
||||
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>`);
|
||||
// Log result to sidebar instead of Popup
|
||||
const targetName = target ? target.name : (action.objetivo === 'todos' ? "El Grupo" : "Tirada de Evento");
|
||||
await this.log("Evento: Prueba", `<b>${targetName}</b> tira <b>${action.tipo_prueba}</b>: Resultado <b style="color:#DAA520">${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);
|
||||
@@ -232,24 +216,17 @@ export class EventInterpreter {
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -265,30 +242,48 @@ export class EventInterpreter {
|
||||
}
|
||||
|
||||
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
|
||||
// RED ALERT: Use applyDamage to handle currentWounds and death/unconscious logic
|
||||
CombatMechanics.applyDamage(h, amount, this.game);
|
||||
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(", ")}`);
|
||||
// Combined outcome into a single log entry
|
||||
await this.log("Efecto", `<b>${msg}</b>. Daño recibido: <b style="color:#ff4444">${amount} Heridas</b> a ${targets.map(t => t.name).join(", ")}`);
|
||||
|
||||
// USER REQUEST: Show a clear notification with the consequence after the delay
|
||||
if (this.game.onShowMessage) {
|
||||
const targetNames = targets.map(t => t.name).join(", ");
|
||||
this.game.onShowMessage("CONSECUENCIA", `<b>${msg}</b><br><br><span style="color:#ff4444; font-size: 20px;">-${amount} Heridas</span> a ${targetNames}`);
|
||||
}
|
||||
} else if (action.tipo === 'oro') {
|
||||
targets.forEach(h => {
|
||||
console.log(`Giving ${amount} Gold to ${h.name}`);
|
||||
h.gold = (h.gold || 0) + amount;
|
||||
h.stats.gold = (h.stats.gold || 0) + amount;
|
||||
if (this.game.onEntityUpdate) this.game.onEntityUpdate(h);
|
||||
});
|
||||
await this.showModal("Oro", `${msg}<br>Ganan <b>${amount}</b> de Oro.`);
|
||||
await this.log("Efecto", `<b>${msg}</b>. Hallazgo: <b style="color:#DAA520">${amount} Oro</b>.`);
|
||||
|
||||
// USER REQUEST: Show a clear notification with the consequence
|
||||
if (this.game.onShowMessage) {
|
||||
const targetNames = targets.map(t => t.name).join(", ");
|
||||
this.game.onShowMessage("HALLAZGO", `<b>${msg}</b><br><br><span style="color:#DAA520; font-size: 20px;">+${amount} Oro</span> para ${targetNames}`);
|
||||
}
|
||||
} else if (action.tipo === 'item') {
|
||||
targets.forEach(h => {
|
||||
if (!h.inventory) h.inventory = [];
|
||||
h.inventory.push(action.id_item);
|
||||
if (this.game.onEntityUpdate) this.game.onEntityUpdate(h);
|
||||
});
|
||||
await this.log("Hallazgo", `<b>${msg}</b>: ${targets.map(t => t.name).join(", ")} obtiene <b>${action.id_item}</b>.`);
|
||||
|
||||
if (this.game.onShowMessage) {
|
||||
this.game.onShowMessage("OBJETO", `<b>${msg}</b>`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleSpawn(action) {
|
||||
// Parse Amount
|
||||
let count = 1;
|
||||
if (typeof action.cantidad === 'string' && action.cantidad.includes('D6')) {
|
||||
const numDice = parseInt(action.cantidad) || 1;
|
||||
@@ -297,84 +292,56 @@ export class EventInterpreter {
|
||||
count = parseInt(action.cantidad) || 1;
|
||||
}
|
||||
|
||||
// Resolve ID/Stats
|
||||
// Map to Monster Definitions?
|
||||
// For now, construct dynamic definition
|
||||
const def = {
|
||||
const monsterId = action.id_monstruo;
|
||||
const libraryDef = MONSTER_DEFINITIONS[monsterId];
|
||||
|
||||
let def;
|
||||
if (libraryDef) {
|
||||
def = { ...libraryDef, stats: { ...libraryDef.stats } };
|
||||
if (action.stats) def.stats = { ...def.stats, ...action.stats };
|
||||
} else {
|
||||
def = {
|
||||
name: action.nombre_fallback || "Enemigo",
|
||||
// Texture mapping based on ID
|
||||
portrait: this.getTexturePathForMonster(action.id_monstruo),
|
||||
portrait: this.getTexturePathForMonster_Legacy(monsterId),
|
||||
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.
|
||||
|
||||
// KEEP MODAL for Spawn - it's a major event that requires immediate player attention
|
||||
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.");
|
||||
await this.showModal("¡Derrumbe!", `¡El techo se viene abajo!<br><b>${collapsedCount}</b> salidas bloqueadas.`);
|
||||
}
|
||||
} 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 if (action.subtipo === 'bloquear_entrada_rastrillo') {
|
||||
if (this.game.blockPortcullisAtEntrance) {
|
||||
this.game.blockPortcullisAtEntrance();
|
||||
}
|
||||
} 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}`;
|
||||
// Legacy fallback ONLY
|
||||
getTexturePathForMonster_Legacy(id) {
|
||||
return "assets/images/dungeon1/standees/enemies/orc.png";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,18 @@ export class CombatSystem {
|
||||
// 3. Update State
|
||||
attacker.hasAttacked = true;
|
||||
|
||||
// Award Gold if hero killed monster
|
||||
if (result.defenderDied && attacker.type === 'hero') {
|
||||
const goldValue = (defender.stats && defender.stats.gold) || 0;
|
||||
if (goldValue > 0) {
|
||||
attacker.stats.gold = (attacker.stats.gold || 0) + goldValue;
|
||||
if (this.game.onEntityUpdate) this.game.onEntityUpdate(attacker);
|
||||
if (this.game.onShowMessage) {
|
||||
this.game.onShowMessage("Botín", `¡Has derrotado a ${defender.name}! Recibes ${goldValue} Oro.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Side Effects (Sound, UI Events)
|
||||
if (window.SOUND_MANAGER) {
|
||||
// Logic to choose sound could be expanded here based on Weapon Type
|
||||
@@ -87,6 +99,18 @@ export class CombatSystem {
|
||||
// 3. Update State
|
||||
attacker.hasAttacked = true;
|
||||
|
||||
// Award Gold if hero killed monster
|
||||
if (result.defenderDied && attacker.type === 'hero') {
|
||||
const goldValue = (defender.stats && defender.stats.gold) || 0;
|
||||
if (goldValue > 0) {
|
||||
attacker.stats.gold = (attacker.stats.gold || 0) + goldValue;
|
||||
if (this.game.onEntityUpdate) this.game.onEntityUpdate(attacker);
|
||||
if (this.game.onShowMessage) {
|
||||
this.game.onShowMessage("Botín", `¡Has derrotado a ${defender.name}! Recibes ${goldValue} Oro.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Side Effects
|
||||
if (window.SOUND_MANAGER) {
|
||||
window.SOUND_MANAGER.playSound('arrow');
|
||||
|
||||
@@ -26,6 +26,7 @@ export class GameEngine {
|
||||
this.plannedPath = []; // Array of {x,y}
|
||||
this.visitedRoomIds = new Set(); // Track tiles triggered
|
||||
this.eventDeck = createEventDeck();
|
||||
this.lastEntranceUsed = null;
|
||||
|
||||
// Callbacks
|
||||
this.onEntityUpdate = null;
|
||||
@@ -62,7 +63,7 @@ export class GameEngine {
|
||||
window.RENDERER.clearAllActiveRings();
|
||||
}
|
||||
this.deselectEntity();
|
||||
this.ai.executeTurn();
|
||||
// Duplicate executeTurn removed here. main.js handles this with playMonsterTurn().
|
||||
}
|
||||
});
|
||||
|
||||
@@ -73,6 +74,13 @@ export class GameEngine {
|
||||
|
||||
// 6. Listen for Power Phase Events
|
||||
this.turnManager.on('POWER_RESULT', (data) => {
|
||||
// Update Wizard Power Stat
|
||||
const wizard = this.heroes.find(h => h.key === 'wizard');
|
||||
if (wizard) {
|
||||
wizard.stats.power = data.roll;
|
||||
if (this.onEntityUpdate) this.onEntityUpdate(wizard);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -175,7 +183,8 @@ export class GameEngine {
|
||||
currentMoves: definition.stats.move,
|
||||
hasAttacked: false,
|
||||
isConscious: true,
|
||||
hasLantern: key === 'barbarian' // Default leader
|
||||
hasLantern: key === 'barbarian', // Default leader
|
||||
inventory: []
|
||||
};
|
||||
|
||||
this.heroes.push(hero);
|
||||
@@ -1550,4 +1559,13 @@ export class GameEngine {
|
||||
window.RENDERER.spawnProp(type, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
blockPortcullisAtEntrance() {
|
||||
if (this.lastEntranceUsed && window.RENDERER) {
|
||||
window.RENDERER.blockDoorWithPortcullis(this.lastEntranceUsed);
|
||||
if (this.onShowMessage) {
|
||||
this.onShowMessage("¡RASTRILLO!", "Un pesado rastrillo de hierro cae a vuestras espaldas, bloqueando la entrada.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,12 @@ export class MagicSystem {
|
||||
|
||||
console.log(`[MagicSystem] Casting ${spell.name} by ${caster.name}`);
|
||||
|
||||
// Deduct Power
|
||||
this.game.turnManager.currentPowerRoll -= spell.cost;
|
||||
// Update Hero Stat for UI
|
||||
caster.stats.power = this.game.turnManager.currentPowerRoll;
|
||||
if (this.game.onEntityUpdate) this.game.onEntityUpdate(caster);
|
||||
|
||||
// Dispatch based on Spell Type
|
||||
// We could also look up a specific handler function map if this grows
|
||||
if (spell.type === 'heal') {
|
||||
|
||||
@@ -21,42 +21,25 @@ export class MonsterAI {
|
||||
// Check for Summoning Sickness / Ambush delay
|
||||
if (monster.skipTurn) {
|
||||
console.log(`[MonsterAI] ${monster.name} (${monster.id}) is gathering its senses (Skipping Turn).`);
|
||||
monster.skipTurn = false; // Ready for next turn
|
||||
|
||||
// Add a small visual delay even if skipping, to show focus?
|
||||
// No, better to just skip significantly to keep flow fast.
|
||||
monster.skipTurn = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.processMonster(monster);
|
||||
// Small "thinking" pause between monsters
|
||||
await new Promise(r => setTimeout(r, 400));
|
||||
await this.actMonster(monster);
|
||||
}
|
||||
}
|
||||
|
||||
processMonster(monster) {
|
||||
return new Promise(resolve => {
|
||||
// NO green ring here - only during attack
|
||||
|
||||
// Calculate delay based on potential move distance to ensure animation finishes
|
||||
// SLOWER: 600ms per tile + Extra buffer for potential attack sequence
|
||||
const moveTime = (monster.stats.move * 600) + 3000; // 3s buffer for attack sequence
|
||||
|
||||
setTimeout(() => {
|
||||
this.actMonster(monster);
|
||||
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, moveTime);
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
actMonster(monster) {
|
||||
async actMonster(monster) {
|
||||
// 1. Check if already adjacent (Engaged) -> ATTACK
|
||||
const adjacentHero = this.getAdjacentHero(monster);
|
||||
|
||||
if (adjacentHero) {
|
||||
console.log(`[MonsterAI] ${monster.id} is engaged with ${adjacentHero.name}. Attacking!`);
|
||||
this.performAttack(monster, adjacentHero);
|
||||
await this.performAttack(monster, adjacentHero);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -96,13 +79,16 @@ export class MonsterAI {
|
||||
// 7. Check if NOW adjacent after move -> ATTACK
|
||||
// Wait for movement animation to complete before checking
|
||||
const movementDuration = actualPath.length * 600;
|
||||
setTimeout(() => {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(async () => {
|
||||
const postMoveHero = this.getAdjacentHero(monster);
|
||||
if (postMoveHero) {
|
||||
console.log(`[MonsterAI] ${monster.id} engaged ${postMoveHero.name} after move. Attacking!`);
|
||||
this.performAttack(monster, postMoveHero);
|
||||
await this.performAttack(monster, postMoveHero);
|
||||
}
|
||||
resolve();
|
||||
}, movementDuration);
|
||||
});
|
||||
}
|
||||
|
||||
getClosestHero(monster) {
|
||||
@@ -212,54 +198,43 @@ export class MonsterAI {
|
||||
return bestPath;
|
||||
}
|
||||
|
||||
performAttack(monster, hero) {
|
||||
// SEQUENCE:
|
||||
// 0. Show TARGET (Blue Ring) on Hero
|
||||
if (this.game.onRangedTarget) {
|
||||
// Re-using onRangedTarget? Or directly calling renderer?
|
||||
// Better to use a specific callback or direct call if available, or just add a new callback.
|
||||
// But let's check if we can access renderer directly or use a new callback.
|
||||
// The user prompt specifically asked for this feature.
|
||||
// I'll assume we can use game.onEntityTarget if defined, or direct renderer call if needed,
|
||||
// but standard pattern here is callbacks.
|
||||
// Let's add onEntityTarget to GameEngine callbacks if not present, but for now I will try to use global RENDERER if possible
|
||||
// OR simply define a new callback `this.game.onEntityTarget(hero.id, true)`.
|
||||
async performAttack(monster, hero) {
|
||||
const numAttacks = monster.stats.attacks || 1;
|
||||
console.log(`[MonsterAI] ${monster.name} performing ${numAttacks} attacks against ${hero.name}`);
|
||||
|
||||
for (let i = 0; i < numAttacks; i++) {
|
||||
if (hero.isDead || (hero.isConscious === false)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Direct renderer call is safest given current context if we don't want to modify GameEngine interface heavily right now.
|
||||
if (window.RENDERER && window.RENDERER.setEntityTarget) {
|
||||
window.RENDERER.setEntityTarget(hero.id, true);
|
||||
}
|
||||
|
||||
const result = CombatMechanics.resolveMeleeAttack(monster, hero, this.game);
|
||||
|
||||
// Step 1: Green ring on attacker
|
||||
if (this.game.onEntityActive) {
|
||||
this.game.onEntityActive(monster.id, true);
|
||||
}
|
||||
|
||||
// Step 2: Attack animation delay (500ms)
|
||||
setTimeout(() => {
|
||||
|
||||
// Step 4: Remove green ring after red ring appears (1200ms for red ring duration)
|
||||
await new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
if (this.game.onEntityActive) {
|
||||
this.game.onEntityActive(monster.id, false);
|
||||
}
|
||||
|
||||
// Remove Target Ring
|
||||
if (window.RENDERER && window.RENDERER.setEntityTarget) {
|
||||
window.RENDERER.setEntityTarget(hero.id, false);
|
||||
}
|
||||
|
||||
// Step 5: Show combat result after both rings are gone
|
||||
setTimeout(() => {
|
||||
if (this.game.onCombatResult) {
|
||||
this.game.onCombatResult(result);
|
||||
}
|
||||
}, 200); // Small delay after rings disappear
|
||||
}, 1200); // Wait for red ring to disappear
|
||||
}, 800); // Attack animation delay + focus time
|
||||
|
||||
// Snappier transition (800ms vs 1500ms)
|
||||
setTimeout(resolve, 800);
|
||||
}, 500); // Wait 500ms for attack "focus"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getAdjacentHero(entity) {
|
||||
|
||||
@@ -61,8 +61,8 @@ export class TurnManager {
|
||||
}
|
||||
|
||||
rollPowerDice() {
|
||||
const roll = Math.floor(Math.random() * 6) + 1;
|
||||
// const roll = 1; // DEBUG: Force Event for testing
|
||||
// const roll = Math.floor(Math.random() * 6) + 1;
|
||||
const roll = 1; // DEBUG: Force Event for testing
|
||||
this.currentPowerRoll = roll;
|
||||
console.log(`Power Roll: ${roll}`);
|
||||
|
||||
|
||||
12
src/main.js
12
src/main.js
@@ -290,6 +290,17 @@ const handleClick = (x, y, doorMesh) => {
|
||||
// 2. Check Adjacency
|
||||
if (game.isLeaderAdjacentToDoor(doorMesh.userData.cells)) {
|
||||
|
||||
// 3. Check Key Requirement for Portcullis
|
||||
if (doorMesh.userData.requiresKey) {
|
||||
const hasKey = game.heroes.some(h => h.inventory && h.inventory.includes('llave_rastrillo'));
|
||||
if (!hasKey) {
|
||||
ui.showModal('Bloqueado', 'Esta puerta tiene un rastrillo bajado. Necesitáis la llave del enano para abrirla.');
|
||||
return;
|
||||
} else {
|
||||
ui.showModal('¡Rastrillo Abierto!', 'Utilizáis la llave del enano para levantar el pesado rastrillo.');
|
||||
}
|
||||
}
|
||||
|
||||
// Open door visually
|
||||
renderer.openDoor(doorMesh);
|
||||
if (window.SOUND_MANAGER) window.SOUND_MANAGER.playSound('door_open');
|
||||
@@ -297,6 +308,7 @@ const handleClick = (x, y, doorMesh) => {
|
||||
// Get proper exit data with direction
|
||||
const exitData = doorMesh.userData.exitData;
|
||||
if (exitData) {
|
||||
game.lastEntranceUsed = exitData;
|
||||
generator.selectDoor(exitData);
|
||||
} else {
|
||||
console.error('[Main] Door missing exitData');
|
||||
|
||||
@@ -204,6 +204,10 @@ export class GameRenderer {
|
||||
this.dungeonRenderer.blockDoor(exitData);
|
||||
}
|
||||
|
||||
blockDoorWithPortcullis(exitData) {
|
||||
this.dungeonRenderer.blockDoorWithPortcullis(exitData);
|
||||
}
|
||||
|
||||
showRangedTargeting(hero, monster, losResult) {
|
||||
this.interactionRenderer.showRangedTargeting(hero, monster, losResult);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { TurnStatusUI } from './ui/TurnStatusUI.js';
|
||||
import { PlacementUI } from './ui/PlacementUI.js';
|
||||
import { FeedbackUI } from './ui/FeedbackUI.js';
|
||||
import { SpellbookUI } from './ui/SpellbookUI.js';
|
||||
import { InventoryUI } from './ui/InventoryUI.js';
|
||||
|
||||
export class UIManager {
|
||||
constructor(cameraManager, gameEngine) {
|
||||
@@ -17,11 +18,13 @@ export class UIManager {
|
||||
this.turnUI = new TurnStatusUI(this.container, gameEngine);
|
||||
this.feedback = new FeedbackUI(this.container, gameEngine);
|
||||
this.spellbook = new SpellbookUI(gameEngine);
|
||||
this.inventory = new InventoryUI(gameEngine);
|
||||
|
||||
// Circular deps / callbacks
|
||||
const cardCallbacks = {
|
||||
showModal: (t, m, c) => this.feedback.showModal(t, m, c),
|
||||
toggleSpellBook: (h) => this.spellbook.toggle(h)
|
||||
toggleSpellBook: (h) => this.spellbook.toggle(h),
|
||||
toggleInventory: (h) => this.inventory.toggle(h)
|
||||
};
|
||||
this.cards = new UnitCardManager(this.container, gameEngine, cardCallbacks);
|
||||
|
||||
@@ -94,6 +97,15 @@ export class UIManager {
|
||||
}
|
||||
};
|
||||
|
||||
// Entity Update (Stats change like Wounds or Gold)
|
||||
const originalUpdate = this.game.onEntityUpdate;
|
||||
this.game.onEntityUpdate = (entity) => {
|
||||
if (originalUpdate) originalUpdate(entity);
|
||||
if (entity.type === 'hero') {
|
||||
this.cards.updateHeroCard(entity.id);
|
||||
}
|
||||
};
|
||||
|
||||
// Turn Manager Events
|
||||
if (this.game.turnManager) {
|
||||
this.game.turnManager.on('phase_changed', (phase) => {
|
||||
|
||||
@@ -306,6 +306,11 @@ export class DungeonRenderer {
|
||||
|
||||
// Load open door texture
|
||||
this.getTexture('/assets/images/dungeon1/doors/door1_open.png', (texture) => {
|
||||
// Safeguard: If it became a portcullis or blocked in the meantime, don't show as open
|
||||
if (doorMesh.userData.isPortcullis || doorMesh.userData.isBlocked) {
|
||||
console.log("[DungeonRenderer] openDoor callback skipped: already portcullis/blocked");
|
||||
return;
|
||||
}
|
||||
doorMesh.material.map = texture;
|
||||
doorMesh.material.needsUpdate = true;
|
||||
doorMesh.userData.isOpen = true;
|
||||
@@ -339,6 +344,46 @@ export class DungeonRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
blockDoorWithPortcullis(exitData) {
|
||||
if (!this.exitGroup || !exitData) return;
|
||||
|
||||
let targetDoor = null;
|
||||
console.log(`[DungeonRenderer] Attempting to block door at ${exitData.x},${exitData.y}`);
|
||||
|
||||
for (const child of this.exitGroup.children) {
|
||||
if (child.userData.isDoor) {
|
||||
for (const cell of child.userData.cells) {
|
||||
if (cell.x === exitData.x && cell.y === exitData.y) {
|
||||
targetDoor = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (targetDoor) break;
|
||||
}
|
||||
|
||||
if (targetDoor) {
|
||||
console.log("[DungeonRenderer] Target door found for portcullis.");
|
||||
this.getTexture('/assets/images/dungeon1/doors/door1_portcullis.png', (texture) => {
|
||||
targetDoor.material.map = texture;
|
||||
targetDoor.material.needsUpdate = true;
|
||||
targetDoor.userData.isBlocked = false;
|
||||
targetDoor.userData.isOpen = false;
|
||||
targetDoor.userData.isPortcullis = true;
|
||||
targetDoor.userData.requiresKey = true;
|
||||
console.log("[DungeonRenderer] Portcullis texture applied.");
|
||||
});
|
||||
} else {
|
||||
console.warn("[DungeonRenderer] Target door NOT found for portcullis at:", exitData);
|
||||
// Debug: Log all door cells
|
||||
this.exitGroup.children.forEach(d => {
|
||||
if (d.userData.isDoor) {
|
||||
console.log(" Door cells:", d.userData.cells.map(c => `${c.x},${c.y}`).join(" | "));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getDoorAtPosition(x, y) {
|
||||
if (!this.exitGroup) return null;
|
||||
for (const child of this.exitGroup.children) {
|
||||
|
||||
145
src/view/ui/InventoryUI.js
Normal file
145
src/view/ui/InventoryUI.js
Normal file
@@ -0,0 +1,145 @@
|
||||
|
||||
export class InventoryUI {
|
||||
constructor(game) {
|
||||
this.game = game;
|
||||
this.container = null;
|
||||
}
|
||||
|
||||
toggle(hero) {
|
||||
if (this.container) {
|
||||
document.body.removeChild(this.container);
|
||||
this.container = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hero) return;
|
||||
|
||||
const container = document.createElement('div');
|
||||
Object.assign(container.style, {
|
||||
position: 'absolute',
|
||||
bottom: '140px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '15px',
|
||||
backgroundColor: 'rgba(30, 20, 10, 0.95)',
|
||||
padding: '20px',
|
||||
borderRadius: '12px',
|
||||
border: '3px solid #8B4513',
|
||||
zIndex: '1500',
|
||||
boxShadow: '0 0 30px rgba(139, 69, 19, 0.6)',
|
||||
minWidth: '300px',
|
||||
maxWidth: '600px',
|
||||
transition: 'all 0.3s ease-out',
|
||||
pointerEvents: 'auto'
|
||||
});
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.textContent = `MOCHILA DE ${hero.name.toUpperCase()}`;
|
||||
Object.assign(title.style, {
|
||||
textAlign: 'center',
|
||||
color: '#DAA520',
|
||||
fontFamily: '"Cinzel", serif',
|
||||
fontSize: '22px',
|
||||
marginBottom: '10px',
|
||||
textShadow: '2px 2px 4px #000',
|
||||
borderBottom: '1px solid #555',
|
||||
paddingBottom: '10px'
|
||||
});
|
||||
container.appendChild(title);
|
||||
|
||||
const itemsContainer = document.createElement('div');
|
||||
Object.assign(itemsContainer.style, {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(80px, 1fr))',
|
||||
gap: '10px',
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto',
|
||||
padding: '5px'
|
||||
});
|
||||
container.appendChild(itemsContainer);
|
||||
|
||||
const inventory = hero.inventory || [];
|
||||
|
||||
if (inventory.length === 0) {
|
||||
const emptyMsg = document.createElement('div');
|
||||
emptyMsg.textContent = "La mochila está vacía...";
|
||||
Object.assign(emptyMsg.style, {
|
||||
textAlign: 'center',
|
||||
color: '#888',
|
||||
fontStyle: 'italic',
|
||||
gridColumn: '1 / -1',
|
||||
padding: '20px'
|
||||
});
|
||||
itemsContainer.appendChild(emptyMsg);
|
||||
} else {
|
||||
inventory.forEach((itemId, index) => {
|
||||
const itemEl = this.createItemElement(itemId);
|
||||
itemsContainer.appendChild(itemEl);
|
||||
});
|
||||
}
|
||||
|
||||
// Close button
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.textContent = 'Cerrar';
|
||||
Object.assign(closeBtn.style, {
|
||||
marginTop: '15px',
|
||||
padding: '8px',
|
||||
backgroundColor: '#444',
|
||||
color: '#fff',
|
||||
border: '1px solid #777',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontFamily: '"Cinzel", serif'
|
||||
});
|
||||
closeBtn.onclick = () => this.toggle();
|
||||
container.appendChild(closeBtn);
|
||||
|
||||
document.body.appendChild(container);
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
createItemElement(itemId) {
|
||||
const item = document.createElement('div');
|
||||
Object.assign(item.style, {
|
||||
width: '80px',
|
||||
height: '80px',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
||||
border: '2px solid #DAA520',
|
||||
borderRadius: '8px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
});
|
||||
|
||||
const icon = document.createElement('div');
|
||||
icon.style.fontSize = '32px';
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.style.fontSize = '10px';
|
||||
label.style.textAlign = 'center';
|
||||
label.style.marginTop = '4px';
|
||||
|
||||
// Item Database (Simple)
|
||||
if (itemId === 'llave_rastrillo') {
|
||||
icon.textContent = '🔑';
|
||||
label.textContent = 'Llave Rastrillo';
|
||||
} else {
|
||||
icon.textContent = '📦';
|
||||
label.textContent = itemId;
|
||||
}
|
||||
|
||||
item.appendChild(icon);
|
||||
item.appendChild(label);
|
||||
|
||||
item.onmouseenter = () => { item.style.backgroundColor = 'rgba(218, 165, 32, 0.2)'; };
|
||||
item.onmouseleave = () => { item.style.backgroundColor = 'rgba(0, 0, 0, 0.6)'; };
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
@@ -199,6 +199,9 @@ export class TurnStatusUI {
|
||||
<div style="font-size: 14px;">
|
||||
Moves: <span style="color: ${hero.currentMoves > 0 ? '#4f4' : '#f44'}; font-weight: bold;">${hero.currentMoves}</span> / ${hero.stats.move}
|
||||
</div>
|
||||
<div style="font-size: 14px;">
|
||||
Oro: <span style="color: #DAA520; font-weight: bold;">${hero.stats.gold || 0}</span> 🪙
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -7,6 +7,7 @@ export class UnitCardManager {
|
||||
this.cardsContainer = null;
|
||||
this.currentHeroCard = null;
|
||||
this.currentMonsterCard = null;
|
||||
this.monsterContainer = null;
|
||||
this.placeholderCard = null;
|
||||
this.attackButton = null;
|
||||
|
||||
@@ -20,10 +21,10 @@ export class UnitCardManager {
|
||||
left: '10px',
|
||||
top: '220px', // Below minimap
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '10px',
|
||||
pointerEvents: 'auto',
|
||||
width: '200px'
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
gap: '15px',
|
||||
pointerEvents: 'auto'
|
||||
});
|
||||
this.parentContainer.appendChild(this.cardsContainer);
|
||||
|
||||
@@ -106,18 +107,18 @@ export class UnitCardManager {
|
||||
const hero = this.game.heroes.find(h => h.id === heroId);
|
||||
if (!hero) return;
|
||||
|
||||
const statsGrid = this.currentHeroCard.querySelector('div[style*="grid-template-columns"]');
|
||||
if (statsGrid) {
|
||||
const statDivs = statsGrid.children;
|
||||
// Assumed order: 4 -> Heridas, 7 -> Movimiento
|
||||
if (statDivs[4]) {
|
||||
const wValue = statDivs[4].querySelector('span:last-child');
|
||||
if (wValue) wValue.textContent = `${hero.currentWounds || hero.stats.wounds}/${hero.stats.wounds}`;
|
||||
}
|
||||
if (statDivs[7]) {
|
||||
const movValue = statDivs[7].querySelector('span:last-child');
|
||||
if (movValue) movValue.textContent = `${hero.currentMoves || 0}/${hero.stats.move}`;
|
||||
}
|
||||
// NEW: Update stats using data-attributes for robustness
|
||||
const updateStat = (key, value) => {
|
||||
const el = this.currentHeroCard.querySelector(`[data-stat="${key}"]`);
|
||||
if (el) el.textContent = value;
|
||||
};
|
||||
|
||||
updateStat('Her', `${hero.currentWounds || hero.stats.wounds}/${hero.stats.wounds}`);
|
||||
updateStat('Mov', `${hero.currentMoves || 0}/${hero.stats.move}`);
|
||||
updateStat('Oro', hero.stats.gold || 0);
|
||||
|
||||
if (hero.key === 'wizard') {
|
||||
updateStat('Pod', hero.stats.power || 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,15 +192,22 @@ export class UnitCardManager {
|
||||
{ label: 'Her', value: `${hero.currentWounds || hero.stats.wounds}/${hero.stats.wounds}` },
|
||||
{ label: 'Ini', value: hero.stats.initiative || 0 },
|
||||
{ label: 'Ata', value: hero.stats.attacks || 0 },
|
||||
{ label: 'Mov', value: `${hero.currentMoves || 0}/${hero.stats.move}` }
|
||||
{ label: 'Mov', value: `${hero.currentMoves || 0}/${hero.stats.move}` },
|
||||
{ label: 'Oro', value: hero.stats.gold || 0 }
|
||||
];
|
||||
|
||||
// USER REQUEST: Show Power for Wizard
|
||||
if (hero.key === 'wizard') {
|
||||
stats.push({ label: 'Pod', value: hero.stats.power || 0 });
|
||||
}
|
||||
|
||||
stats.forEach(stat => {
|
||||
const el = document.createElement('div');
|
||||
Object.assign(el.style, { backgroundColor: 'rgba(0, 0, 0, 0.5)', padding: '3px 5px', borderRadius: '3px', display: 'flex', justifyContent: 'space-between' });
|
||||
|
||||
const l = document.createElement('span'); l.textContent = stat.label + ':'; l.style.color = '#AAA';
|
||||
const v = document.createElement('span'); v.textContent = stat.value; v.style.color = '#FFF'; v.style.fontWeight = 'bold';
|
||||
v.dataset.stat = stat.label; // Add data attribute for easier updates
|
||||
|
||||
el.appendChild(l); el.appendChild(v);
|
||||
statsGrid.appendChild(el);
|
||||
@@ -280,11 +288,14 @@ export class UnitCardManager {
|
||||
const invBtn = document.createElement('button');
|
||||
invBtn.textContent = '🎒 INVENTARIO';
|
||||
Object.assign(invBtn.style, {
|
||||
width: '100%', padding: '8px', marginTop: '8px', backgroundColor: '#444',
|
||||
color: '#fff', border: '1px solid #777', borderRadius: '4px',
|
||||
fontFamily: '"Cinzel", serif', fontSize: '12px', cursor: 'pointer' // Changed cursor to pointer for feel, though functionality implies future
|
||||
width: '100%', padding: '8px', marginTop: '8px', backgroundColor: '#5D4037',
|
||||
color: '#fff', border: '1px solid #8B4513', borderRadius: '4px',
|
||||
fontFamily: '"Cinzel", serif', fontSize: '12px', cursor: 'pointer'
|
||||
});
|
||||
invBtn.title = 'Inventario (Próximamente)';
|
||||
invBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
if (this.callbacks.toggleInventory) this.callbacks.toggleInventory(hero);
|
||||
};
|
||||
card.appendChild(invBtn);
|
||||
|
||||
|
||||
@@ -365,8 +376,17 @@ export class UnitCardManager {
|
||||
|
||||
showMonsterCard(monster) {
|
||||
this.hideMonsterCard();
|
||||
|
||||
// Create a sub-container for monster card + button
|
||||
this.monsterContainer = document.createElement('div');
|
||||
Object.assign(this.monsterContainer.style, {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px'
|
||||
});
|
||||
|
||||
this.currentMonsterCard = this.createMonsterCard(monster);
|
||||
this.cardsContainer.appendChild(this.currentMonsterCard);
|
||||
this.monsterContainer.appendChild(this.currentMonsterCard);
|
||||
|
||||
this.attackButton = document.createElement('button');
|
||||
this.attackButton.textContent = '⚔️ ATACAR';
|
||||
@@ -384,7 +404,6 @@ export class UnitCardManager {
|
||||
const result = this.game.performHeroAttack(monster.id);
|
||||
if (result && result.success) {
|
||||
this.hideMonsterCard();
|
||||
// Optional: deselect monster logic if managed externally
|
||||
if (this.game.selectedMonster) {
|
||||
if (this.game.onEntitySelect) this.game.onEntitySelect(this.game.selectedMonster.id, false);
|
||||
this.game.selectedMonster = null;
|
||||
@@ -392,7 +411,8 @@ export class UnitCardManager {
|
||||
}
|
||||
}
|
||||
};
|
||||
this.cardsContainer.appendChild(this.attackButton);
|
||||
this.monsterContainer.appendChild(this.attackButton);
|
||||
this.cardsContainer.appendChild(this.monsterContainer);
|
||||
}
|
||||
|
||||
showRangedAttackUI(monster) {
|
||||
@@ -417,13 +437,11 @@ export class UnitCardManager {
|
||||
}
|
||||
|
||||
hideMonsterCard() {
|
||||
if (this.currentMonsterCard && this.currentMonsterCard.parentNode) {
|
||||
this.cardsContainer.removeChild(this.currentMonsterCard);
|
||||
this.currentMonsterCard = null;
|
||||
if (this.monsterContainer && this.monsterContainer.parentNode) {
|
||||
this.cardsContainer.removeChild(this.monsterContainer);
|
||||
}
|
||||
if (this.attackButton && this.attackButton.parentNode) {
|
||||
this.cardsContainer.removeChild(this.attackButton);
|
||||
this.monsterContainer = null;
|
||||
this.currentMonsterCard = null;
|
||||
this.attackButton = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user