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 {
Moves: ${hero.currentMoves} / ${hero.stats.move}
+
+ Oro: ${hero.stats.gold || 0} 🪙 +
`; diff --git a/src/view/ui/UnitCardManager.js b/src/view/ui/UnitCardManager.js index 413a036..d48c8d9 100644 --- a/src/view/ui/UnitCardManager.js +++ b/src/view/ui/UnitCardManager.js @@ -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.attackButton && this.attackButton.parentNode) { - this.cardsContainer.removeChild(this.attackButton); - this.attackButton = null; + if (this.monsterContainer && this.monsterContainer.parentNode) { + this.cardsContainer.removeChild(this.monsterContainer); } + this.monsterContainer = null; + this.currentMonsterCard = null; + this.attackButton = null; } }