diff --git a/DEVLOG.md b/DEVLOG.md
index 539f349..e9039ab 100644
--- a/DEVLOG.md
+++ b/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
diff --git a/public/assets/images/dungeon1/doors/door1_portcullis.png b/public/assets/images/dungeon1/doors/door1_portcullis.png
new file mode 100644
index 0000000..afb0c58
Binary files /dev/null and b/public/assets/images/dungeon1/doors/door1_portcullis.png differ
diff --git a/public/assets/images/dungeon1/standees/enemies/Lordwarlock.png b/public/assets/images/dungeon1/standees/enemies/Lordwarlock.png
index f429974..25ecac4 100644
Binary files a/public/assets/images/dungeon1/standees/enemies/Lordwarlock.png and b/public/assets/images/dungeon1/standees/enemies/Lordwarlock.png differ
diff --git a/public/assets/images/dungeon1/standees/enemies/Lordwarlock_1.png b/public/assets/images/dungeon1/standees/enemies/Lordwarlock_1.png
new file mode 100644
index 0000000..31c506c
Binary files /dev/null and b/public/assets/images/dungeon1/standees/enemies/Lordwarlock_1.png differ
diff --git a/public/assets/images/dungeon1/standees/enemies/Lordwarlock_bak.png b/public/assets/images/dungeon1/standees/enemies/Lordwarlock_bak.png
new file mode 100644
index 0000000..f429974
Binary files /dev/null and b/public/assets/images/dungeon1/standees/enemies/Lordwarlock_bak.png differ
diff --git a/public/assets/images/dungeon1/standees/enemies/araña_gigante.png b/public/assets/images/dungeon1/standees/enemies/araña_gigante.png
deleted file mode 100644
index d7881ff..0000000
Binary files a/public/assets/images/dungeon1/standees/enemies/araña_gigante.png and /dev/null differ
diff --git a/src/engine/data/EventCards.js b/src/engine/data/EventCards.js
index 0108dab..aef1767 100644
--- a/src/engine/data/EventCards.js
+++ b/src/engine/data/EventCards.js
@@ -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"
}
]
}
diff --git a/src/engine/data/Events.js b/src/engine/data/Events.js
index 59a7531..6686997 100644
--- a/src/engine/data/Events.js
+++ b/src/engine/data/Events.js
@@ -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.");
}
*/
diff --git a/src/engine/data/Heroes.js b/src/engine/data/Heroes.js
index 6887522..80110ee 100644
--- a/src/engine/data/Heroes.js
+++ b/src/engine/data/Heroes.js
@@ -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
}
}
};
diff --git a/src/engine/data/Monsters.js b/src/engine/data/Monsters.js
index 019b6f6..2f6ffc9 100644
--- a/src/engine/data/Monsters.js
+++ b/src/engine/data/Monsters.js
@@ -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
+ }
}
};
diff --git a/src/engine/events/EventInterpreter.js b/src/engine/events/EventInterpreter.js
index cb811bc..39cf54e 100644
--- a/src/engine/events/EventInterpreter.js
+++ b/src/engine/events/EventInterpreter.js
@@ -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}
Objetivo: ${names}`);
+ // Improved immersive message: "¡El Bárbaro ha pisado una trampa!"
+ await this.log("Selección", `¡El ${names} ${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", `${targetName} realiza una prueba de ${action.tipo_prueba}...
Resultado: ${roll}`);
+ // 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", `${targetName} tira ${action.tipo_prueba}: Resultado ${roll}`);
// 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}
${amount} Heridas a ${targets.map(t => t.name).join(", ")}`);
+ // Combined outcome into a single log entry
+ await this.log("Efecto", `${msg}. Daño recibido: ${amount} Heridas 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", `${msg}
-${amount} Heridas 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}
Ganan ${amount} de Oro.`);
+ await this.log("Efecto", `${msg}. Hallazgo: ${amount} Oro.`);
+
+ // 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", `${msg}
+${amount} Oro 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", `${msg}: ${targets.map(t => t.name).join(", ")} obtiene ${action.id_item}.`);
+
+ if (this.game.onShowMessage) {
+ this.game.onShowMessage("OBJETO", `${msg}`);
+ }
}
}
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 = {
- 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 }
- };
+ 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",
+ 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 ${count} ${def.name}!
¡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!
${collapsedCount} 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!
${collapsedCount} 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";
}
}
diff --git a/src/engine/game/CombatSystem.js b/src/engine/game/CombatSystem.js
index 9c40c4a..283aea8 100644
--- a/src/engine/game/CombatSystem.js
+++ b/src/engine/game/CombatSystem.js
@@ -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');
diff --git a/src/engine/game/GameEngine.js b/src/engine/game/GameEngine.js
index 7aa63e9..01cd463 100644
--- a/src/engine/game/GameEngine.js
+++ b/src/engine/game/GameEngine.js
@@ -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.");
+ }
+ }
+ }
}
diff --git a/src/engine/game/MagicSystem.js b/src/engine/game/MagicSystem.js
index 1519b4a..05622cc 100644
--- a/src/engine/game/MagicSystem.js
+++ b/src/engine/game/MagicSystem.js
@@ -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') {
diff --git a/src/engine/game/MonsterAI.js b/src/engine/game/MonsterAI.js
index 6c5b21d..ea10a79 100644
--- a/src/engine/game/MonsterAI.js
+++ b/src/engine/game/MonsterAI.js
@@ -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(() => {
- const postMoveHero = this.getAdjacentHero(monster);
- if (postMoveHero) {
- console.log(`[MonsterAI] ${monster.id} engaged ${postMoveHero.name} after move. Attacking!`);
- this.performAttack(monster, postMoveHero);
- }
- }, movementDuration);
+ await new Promise(resolve => {
+ setTimeout(async () => {
+ const postMoveHero = this.getAdjacentHero(monster);
+ if (postMoveHero) {
+ console.log(`[MonsterAI] ${monster.id} engaged ${postMoveHero.name} after move. Attacking!`);
+ 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}`);
- // 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);
- }
+ for (let i = 0; i < numAttacks; i++) {
+ if (hero.isDead || (hero.isConscious === false)) {
+ break;
+ }
- const result = CombatMechanics.resolveMeleeAttack(monster, hero, this.game);
+ if (window.RENDERER && window.RENDERER.setEntityTarget) {
+ window.RENDERER.setEntityTarget(hero.id, true);
+ }
- // Step 1: Green ring on attacker
- if (this.game.onEntityActive) {
- this.game.onEntityActive(monster.id, true);
- }
+ const result = CombatMechanics.resolveMeleeAttack(monster, hero, this.game);
- // Step 2: Attack animation delay (500ms)
- setTimeout(() => {
+ if (this.game.onEntityActive) {
+ this.game.onEntityActive(monster.id, true);
+ }
- // Step 4: Remove green ring after red ring appears (1200ms for red ring duration)
- 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
+ await new Promise(resolve => {
setTimeout(() => {
+ if (this.game.onEntityActive) {
+ this.game.onEntityActive(monster.id, false);
+ }
+ if (window.RENDERER && window.RENDERER.setEntityTarget) {
+ window.RENDERER.setEntityTarget(hero.id, false);
+ }
+
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) {
diff --git a/src/engine/game/TurnManager.js b/src/engine/game/TurnManager.js
index bc41b08..160dde1 100644
--- a/src/engine/game/TurnManager.js
+++ b/src/engine/game/TurnManager.js
@@ -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}`);
diff --git a/src/main.js b/src/main.js
index 478c349..73464e2 100644
--- a/src/main.js
+++ b/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');
diff --git a/src/view/GameRenderer.js b/src/view/GameRenderer.js
index 25688cc..49495ca 100644
--- a/src/view/GameRenderer.js
+++ b/src/view/GameRenderer.js
@@ -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);
}
diff --git a/src/view/UIManager.js b/src/view/UIManager.js
index c3d8d63..6680c47 100644
--- a/src/view/UIManager.js
+++ b/src/view/UIManager.js
@@ -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) => {
diff --git a/src/view/render/DungeonRenderer.js b/src/view/render/DungeonRenderer.js
index 44a6433..cd72956 100644
--- a/src/view/render/DungeonRenderer.js
+++ b/src/view/render/DungeonRenderer.js
@@ -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) {
diff --git a/src/view/ui/InventoryUI.js b/src/view/ui/InventoryUI.js
new file mode 100644
index 0000000..b06b210
--- /dev/null
+++ b/src/view/ui/InventoryUI.js
@@ -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;
+ }
+}
diff --git a/src/view/ui/TurnStatusUI.js b/src/view/ui/TurnStatusUI.js
index 8b096e9..25d97d0 100644
--- a/src/view/ui/TurnStatusUI.js
+++ b/src/view/ui/TurnStatusUI.js
@@ -199,6 +199,9 @@ export class TurnStatusUI {