Sesión 15: Implementación de Inventario y lógica de evento de Rastrillo/Llave

This commit is contained in:
2026-01-10 17:41:32 +01:00
parent 82bdcacf95
commit 83882b25ba
23 changed files with 588 additions and 273 deletions

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 646 KiB

View File

@@ -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"
}
]
}

View File

@@ -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.");
}
*/

View File

@@ -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
}
}
};

View File

@@ -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
}
}
};

View File

@@ -1,9 +1,5 @@
/**
* EventInterpreter.js
*
* Takes high-level Action Instructions from Event Cards (JSON)
* and executes them using the Game Engine's low-level systems.
*/
import { MONSTER_DEFINITIONS } from '../data/Monsters.js';
import { CombatMechanics } from '../game/CombatMechanics.js';
export class EventInterpreter {
constructor(gameEngine) {
@@ -72,24 +68,19 @@ export class EventInterpreter {
});
}
async log(title, text) {
if (this.game.onShowMessage) {
this.game.onShowMessage(title, text);
}
// Delay removed here, pacing is now handled only by the queue
return Promise.resolve();
}
async processQueue(onComplete = null) {
if (this.isProcessing) return;
if (this.queue.length === 0) {
console.log("[EventInterpreter] All actions completed.");
// Event Log Summary
if (this.game.onShowMessage) {
// We use onShowMessage as Log (via main.js filtering) or specialized logger
// Actually, let's inject a "System Log" call if possible.
// Main.js redirects "Efecto" titles to log, but let's be explicit.
// Using generic onShowMessage with a distinct title for logging.
// Or better, let's call a log method if exposed on game.
// Let's assume onShowMessage("LOG", ...) goes to log if we tweak main.js,
// OR we just use a title that main.js recognizes as loggable.
// But wait, the USER wants a log of the CARD itself.
}
if (onComplete) {
onComplete();
} else {
@@ -109,9 +100,9 @@ export class EventInterpreter {
this.isProcessing = false;
// Next step
// Increased delay to 2s between major event steps as requested
if (this.queue.length > 0) {
setTimeout(() => this.processQueue(onComplete), 500);
setTimeout(() => this.processQueue(onComplete), 2000);
} else {
this.processQueue(onComplete); // Finish up
}
@@ -120,7 +111,7 @@ export class EventInterpreter {
async executeAction(action) {
switch (action.tipo_accion) {
case 'MENSAJE':
await this.showModal("Evento", action.texto || action.mensaje);
await this.log("Evento", action.texto || action.mensaje);
break;
case 'SELECCION':
@@ -167,14 +158,11 @@ export class EventInterpreter {
const idx = Math.floor(Math.random() * this.game.heroes.length);
targets = [this.game.heroes[idx]];
} else if (action.modo === 'tirada_baja') {
// Roll D6 for each, pick lowest
// For now, SIMULATED logic without UI prompts
let lowest = 99;
let candidates = [];
this.game.heroes.forEach(h => {
const roll = Math.floor(Math.random() * 6) + 1;
// console.log(`${h.name} rolled ${roll}`);
if (roll < lowest) {
lowest = roll;
candidates = [h];
@@ -182,24 +170,22 @@ export class EventInterpreter {
candidates.push(h); // Tie
}
});
// If tie, pick random from candidates
targets = [candidates[Math.floor(Math.random() * candidates.length)]];
}
// Store result
if (action.guardar_como) {
this.currentContext[action.guardar_como] = targets[0]; // Simplification for Single Target
this.currentContext[action.guardar_como] = targets[0];
}
if (action.mensaje) {
const names = targets.map(t => t.name).join(", ");
// BLOCKING MODAL
await this.showModal("Selección", `${action.mensaje}<br><br><b>Objetivo: ${names}</b>`);
// Improved immersive message: "¡El Bárbaro ha pisado una trampa!"
await this.log("Selección", `¡El <b>${names}</b> ${action.mensaje}`);
}
}
async handleTest(action) {
// Resolve target
const target = action.origen ? this.currentContext[action.origen] : null;
if (!target && action.origen) {
console.error("Test target not found in context:", action.origen);
@@ -207,21 +193,19 @@ export class EventInterpreter {
}
let roll = 0;
// Parse dice string "1D6", "2D6"
if (action.tipo_prueba.includes('D6')) {
const count = parseInt(action.tipo_prueba) || 1;
for (let i = 0; i < count; i++) roll += Math.floor(Math.random() * 6) + 1;
}
// BLOCKING MODAL: Show the roll result
const targetName = target ? target.name : "Nadie";
await this.showModal("Prueba", `<b>${targetName}</b> realiza una prueba de <b>${action.tipo_prueba}</b>...<br>Resultado: <b>${roll}</b>`);
// Log result to sidebar instead of Popup
const targetName = target ? target.name : (action.objetivo === 'todos' ? "El Grupo" : "Tirada de Evento");
await this.log("Evento: Prueba", `<b>${targetName}</b> tira <b>${action.tipo_prueba}</b>: Resultado <b style="color:#DAA520">${roll}</b>`);
// Check Table
if (action.tabla) {
let resultActions = null;
// Iterate keys "1", "2-3", "4-6"
for (const key of Object.keys(action.tabla)) {
if (key.includes('-')) {
const [min, max] = key.split('-').map(Number);
@@ -232,24 +216,17 @@ export class EventInterpreter {
}
if (resultActions) {
// Prepend these new actions to the FRONT of the queue to execute immediately
console.log(`Test Roll: ${roll} -> Result found`, resultActions);
this.queue.unshift(...resultActions);
}
}
}
async handleEffect(action) {
// Resolve Target
let targets = [];
if (action.objetivo) {
if (this.currentContext[action.objetivo]) targets = [this.currentContext[action.objetivo]];
} else {
// Implicit context? Default to all?
// Better to be explicit in JSON mostly
}
// Parse Count
let amount = 0;
let isDice = false;
if (typeof action.cantidad === 'number') amount = action.cantidad;
@@ -265,30 +242,48 @@ export class EventInterpreter {
}
let msg = action.mensaje || "Efecto aplicado";
if (isDice) msg += ` (Dado: ${amount})`;
if (action.tipo === 'daño') {
targets.forEach(h => {
// Apply Damage Logic
// this.game.combatSystem.applyDamage(h, amount)...
console.log(`Applying ${amount} Damage to ${h.name}`);
h.stats.wounds -= amount; // Simple direct manipulation for now
// RED ALERT: Use applyDamage to handle currentWounds and death/unconscious logic
CombatMechanics.applyDamage(h, amount, this.game);
if (this.game.onEntityUpdate) this.game.onEntityUpdate(h);
// Visual feedback?
});
await this.showModal("Daño", `${msg}<br><b>${amount} Heridas</b> a ${targets.map(t => t.name).join(", ")}`);
// Combined outcome into a single log entry
await this.log("Efecto", `<b>${msg}</b>. Daño recibido: <b style="color:#ff4444">${amount} Heridas</b> a ${targets.map(t => t.name).join(", ")}`);
// USER REQUEST: Show a clear notification with the consequence after the delay
if (this.game.onShowMessage) {
const targetNames = targets.map(t => t.name).join(", ");
this.game.onShowMessage("CONSECUENCIA", `<b>${msg}</b><br><br><span style="color:#ff4444; font-size: 20px;">-${amount} Heridas</span> a ${targetNames}`);
}
} else if (action.tipo === 'oro') {
targets.forEach(h => {
console.log(`Giving ${amount} Gold to ${h.name}`);
h.gold = (h.gold || 0) + amount;
h.stats.gold = (h.stats.gold || 0) + amount;
if (this.game.onEntityUpdate) this.game.onEntityUpdate(h);
});
await this.showModal("Oro", `${msg}<br>Ganan <b>${amount}</b> de Oro.`);
await this.log("Efecto", `<b>${msg}</b>. Hallazgo: <b style="color:#DAA520">${amount} Oro</b>.`);
// USER REQUEST: Show a clear notification with the consequence
if (this.game.onShowMessage) {
const targetNames = targets.map(t => t.name).join(", ");
this.game.onShowMessage("HALLAZGO", `<b>${msg}</b><br><br><span style="color:#DAA520; font-size: 20px;">+${amount} Oro</span> para ${targetNames}`);
}
} else if (action.tipo === 'item') {
targets.forEach(h => {
if (!h.inventory) h.inventory = [];
h.inventory.push(action.id_item);
if (this.game.onEntityUpdate) this.game.onEntityUpdate(h);
});
await this.log("Hallazgo", `<b>${msg}</b>: ${targets.map(t => t.name).join(", ")} obtiene <b>${action.id_item}</b>.`);
if (this.game.onShowMessage) {
this.game.onShowMessage("OBJETO", `<b>${msg}</b>`);
}
}
}
async handleSpawn(action) {
// Parse Amount
let count = 1;
if (typeof action.cantidad === 'string' && action.cantidad.includes('D6')) {
const numDice = parseInt(action.cantidad) || 1;
@@ -297,84 +292,56 @@ export class EventInterpreter {
count = parseInt(action.cantidad) || 1;
}
// Resolve ID/Stats
// Map to Monster Definitions?
// For now, construct dynamic definition
const def = {
const monsterId = action.id_monstruo;
const libraryDef = MONSTER_DEFINITIONS[monsterId];
let def;
if (libraryDef) {
def = { ...libraryDef, stats: { ...libraryDef.stats } };
if (action.stats) def.stats = { ...def.stats, ...action.stats };
} else {
def = {
name: action.nombre_fallback || "Enemigo",
// Texture mapping based on ID
portrait: this.getTexturePathForMonster(action.id_monstruo),
portrait: this.getTexturePathForMonster_Legacy(monsterId),
stats: action.stats || { wounds: 1, move: 4, ws: 3, str: 3, toughness: 3 }
};
}
// Check if there is an Exploration Context (TileID)
let contextTileId = null;
if (this.game.currentEventContext && this.game.currentEventContext.tileId) {
contextTileId = this.game.currentEventContext.tileId;
}
const spots = this.game.findSpawnPoints(count, contextTileId);
spots.forEach(spot => {
// Monsters spawned via Event Card in Exploration phase should NOT skip turn?
// "Monsters placed... move and attack as described in Power Phase" -> Wait.
// Power Phase monsters ATTACK immediately. Exploration monsters DO NOT?
// Rules say: "If Monsters, place them... see Power Phase for details"
// Actually, "In the Monster Phase... determine how many... place them... Keep the card handy... information needed later"
// Usually ambushes act immediately, but room dwellers act in the Monster Phase.
// Since we are triggering this AT THE START of Monster Phase, they will act in this phase naturally ONLY IF we don't skip turn.
// However, GameEngine monster AI loop iterates all monsters.
// Newly added monster might be picked up immediately if added to the array?
// Yes, standard is they act.
this.game.spawnMonster(def, spot.x, spot.y, { skipTurn: false });
});
// Clear context after use to prevent leakage?
// Or keep it for the duration of the event chain?
// Safest to keep until event ends, GameEngine clears it?
// We leave it, GameEngine overrides or clears it when needed.
// KEEP MODAL for Spawn - it's a major event that requires immediate player attention
await this.showModal("¡Emboscada!", `Aparecen <b>${count} ${def.name}</b>!<br>¡Prepárate para luchar!`);
}
async handleEnvironment(action) {
if (action.subtipo === 'bloquear_salidas_excepto_entrada') {
console.log("[Event] Collapsing Exits...");
if (this.game.collapseExits) {
const collapsedCount = this.game.collapseExits();
await this.showModal("¡Derrumbe!", `¡El techo se viene abajo!<br><b>${collapsedCount}</b> salidas han quedado bloqueadas por escombros.`);
} else {
console.error("GameEngine.collapseExits not implemented!");
await this.showModal("Error", "GameEngine.collapseExits no implementado.");
await this.showModal("¡Derrumbe!", `¡El techo se viene abajo!<br><b>${collapsedCount}</b> salidas bloqueadas.`);
}
} else if (action.subtipo === 'colocar_marcador') {
console.log(`[Event] Placing Marker: ${action.marcador}`);
if (this.game.placeEventMarker) {
// Determine position based on context (e.g. center of current room)
// For now, pass null so GameEngine decides based on player location
this.game.placeEventMarker(action.marcador);
}
} else if (action.subtipo === 'bloquear_entrada_rastrillo') {
if (this.game.blockPortcullisAtEntrance) {
this.game.blockPortcullisAtEntrance();
}
} else {
console.log("Environment Action Unknown:", action.subtipo);
}
}
getTexturePathForMonster(id) {
// Map JSON IDs to Files
// id_monstruo: "minotaur" -> "dungeon1/standees/enemies/minotaur.png" (if exists) or fallback
// We checked file list earlier:
// bat.png, goblin.png, orc.png, skaven.png, chaosWarrior.png, Lordwarlock.png, rat.png, spiderGiant.png
const map = {
"giant_spider": "spiderGiant.png",
"orc": "orc.png",
"goblin": "goblin.png",
"skaven": "skaven.png",
"minotaur": "minotaur.png"
};
const filename = map[id] || "orc.png";
return `assets/images/dungeon1/standees/enemies/${filename}`;
// Legacy fallback ONLY
getTexturePathForMonster_Legacy(id) {
return "assets/images/dungeon1/standees/enemies/orc.png";
}
}

View File

@@ -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');

View File

@@ -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.");
}
}
}
}

View File

@@ -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') {

View File

@@ -21,42 +21,25 @@ export class MonsterAI {
// Check for Summoning Sickness / Ambush delay
if (monster.skipTurn) {
console.log(`[MonsterAI] ${monster.name} (${monster.id}) is gathering its senses (Skipping Turn).`);
monster.skipTurn = false; // Ready for next turn
// Add a small visual delay even if skipping, to show focus?
// No, better to just skip significantly to keep flow fast.
monster.skipTurn = false;
continue;
}
await this.processMonster(monster);
// Small "thinking" pause between monsters
await new Promise(r => setTimeout(r, 400));
await this.actMonster(monster);
}
}
processMonster(monster) {
return new Promise(resolve => {
// NO green ring here - only during attack
// Calculate delay based on potential move distance to ensure animation finishes
// SLOWER: 600ms per tile + Extra buffer for potential attack sequence
const moveTime = (monster.stats.move * 600) + 3000; // 3s buffer for attack sequence
setTimeout(() => {
this.actMonster(monster);
setTimeout(() => {
resolve();
}, moveTime);
}, 100);
});
}
actMonster(monster) {
async actMonster(monster) {
// 1. Check if already adjacent (Engaged) -> ATTACK
const adjacentHero = this.getAdjacentHero(monster);
if (adjacentHero) {
console.log(`[MonsterAI] ${monster.id} is engaged with ${adjacentHero.name}. Attacking!`);
this.performAttack(monster, adjacentHero);
await this.performAttack(monster, adjacentHero);
return;
}
@@ -96,13 +79,16 @@ export class MonsterAI {
// 7. Check if NOW adjacent after move -> ATTACK
// Wait for movement animation to complete before checking
const movementDuration = actualPath.length * 600;
setTimeout(() => {
await new Promise(resolve => {
setTimeout(async () => {
const postMoveHero = this.getAdjacentHero(monster);
if (postMoveHero) {
console.log(`[MonsterAI] ${monster.id} engaged ${postMoveHero.name} after move. Attacking!`);
this.performAttack(monster, postMoveHero);
await this.performAttack(monster, postMoveHero);
}
resolve();
}, movementDuration);
});
}
getClosestHero(monster) {
@@ -212,54 +198,43 @@ export class MonsterAI {
return bestPath;
}
performAttack(monster, hero) {
// SEQUENCE:
// 0. Show TARGET (Blue Ring) on Hero
if (this.game.onRangedTarget) {
// Re-using onRangedTarget? Or directly calling renderer?
// Better to use a specific callback or direct call if available, or just add a new callback.
// But let's check if we can access renderer directly or use a new callback.
// The user prompt specifically asked for this feature.
// I'll assume we can use game.onEntityTarget if defined, or direct renderer call if needed,
// but standard pattern here is callbacks.
// Let's add onEntityTarget to GameEngine callbacks if not present, but for now I will try to use global RENDERER if possible
// OR simply define a new callback `this.game.onEntityTarget(hero.id, true)`.
async performAttack(monster, hero) {
const numAttacks = monster.stats.attacks || 1;
console.log(`[MonsterAI] ${monster.name} performing ${numAttacks} attacks against ${hero.name}`);
for (let i = 0; i < numAttacks; i++) {
if (hero.isDead || (hero.isConscious === false)) {
break;
}
// Direct renderer call is safest given current context if we don't want to modify GameEngine interface heavily right now.
if (window.RENDERER && window.RENDERER.setEntityTarget) {
window.RENDERER.setEntityTarget(hero.id, true);
}
const result = CombatMechanics.resolveMeleeAttack(monster, hero, this.game);
// Step 1: Green ring on attacker
if (this.game.onEntityActive) {
this.game.onEntityActive(monster.id, true);
}
// Step 2: Attack animation delay (500ms)
setTimeout(() => {
// Step 4: Remove green ring after red ring appears (1200ms for red ring duration)
await new Promise(resolve => {
setTimeout(() => {
if (this.game.onEntityActive) {
this.game.onEntityActive(monster.id, false);
}
// Remove Target Ring
if (window.RENDERER && window.RENDERER.setEntityTarget) {
window.RENDERER.setEntityTarget(hero.id, false);
}
// Step 5: Show combat result after both rings are gone
setTimeout(() => {
if (this.game.onCombatResult) {
this.game.onCombatResult(result);
}
}, 200); // Small delay after rings disappear
}, 1200); // Wait for red ring to disappear
}, 800); // Attack animation delay + focus time
// Snappier transition (800ms vs 1500ms)
setTimeout(resolve, 800);
}, 500); // Wait 500ms for attack "focus"
});
}
}
getAdjacentHero(entity) {

View File

@@ -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}`);

View File

@@ -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');

View File

@@ -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);
}

View File

@@ -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) => {

View File

@@ -306,6 +306,11 @@ export class DungeonRenderer {
// Load open door texture
this.getTexture('/assets/images/dungeon1/doors/door1_open.png', (texture) => {
// Safeguard: If it became a portcullis or blocked in the meantime, don't show as open
if (doorMesh.userData.isPortcullis || doorMesh.userData.isBlocked) {
console.log("[DungeonRenderer] openDoor callback skipped: already portcullis/blocked");
return;
}
doorMesh.material.map = texture;
doorMesh.material.needsUpdate = true;
doorMesh.userData.isOpen = true;
@@ -339,6 +344,46 @@ export class DungeonRenderer {
}
}
blockDoorWithPortcullis(exitData) {
if (!this.exitGroup || !exitData) return;
let targetDoor = null;
console.log(`[DungeonRenderer] Attempting to block door at ${exitData.x},${exitData.y}`);
for (const child of this.exitGroup.children) {
if (child.userData.isDoor) {
for (const cell of child.userData.cells) {
if (cell.x === exitData.x && cell.y === exitData.y) {
targetDoor = child;
break;
}
}
}
if (targetDoor) break;
}
if (targetDoor) {
console.log("[DungeonRenderer] Target door found for portcullis.");
this.getTexture('/assets/images/dungeon1/doors/door1_portcullis.png', (texture) => {
targetDoor.material.map = texture;
targetDoor.material.needsUpdate = true;
targetDoor.userData.isBlocked = false;
targetDoor.userData.isOpen = false;
targetDoor.userData.isPortcullis = true;
targetDoor.userData.requiresKey = true;
console.log("[DungeonRenderer] Portcullis texture applied.");
});
} else {
console.warn("[DungeonRenderer] Target door NOT found for portcullis at:", exitData);
// Debug: Log all door cells
this.exitGroup.children.forEach(d => {
if (d.userData.isDoor) {
console.log(" Door cells:", d.userData.cells.map(c => `${c.x},${c.y}`).join(" | "));
}
});
}
}
getDoorAtPosition(x, y) {
if (!this.exitGroup) return null;
for (const child of this.exitGroup.children) {

145
src/view/ui/InventoryUI.js Normal file
View File

@@ -0,0 +1,145 @@
export class InventoryUI {
constructor(game) {
this.game = game;
this.container = null;
}
toggle(hero) {
if (this.container) {
document.body.removeChild(this.container);
this.container = null;
return;
}
if (!hero) return;
const container = document.createElement('div');
Object.assign(container.style, {
position: 'absolute',
bottom: '140px',
left: '50%',
transform: 'translateX(-50%)',
display: 'flex',
flexDirection: 'column',
gap: '15px',
backgroundColor: 'rgba(30, 20, 10, 0.95)',
padding: '20px',
borderRadius: '12px',
border: '3px solid #8B4513',
zIndex: '1500',
boxShadow: '0 0 30px rgba(139, 69, 19, 0.6)',
minWidth: '300px',
maxWidth: '600px',
transition: 'all 0.3s ease-out',
pointerEvents: 'auto'
});
const title = document.createElement('div');
title.textContent = `MOCHILA DE ${hero.name.toUpperCase()}`;
Object.assign(title.style, {
textAlign: 'center',
color: '#DAA520',
fontFamily: '"Cinzel", serif',
fontSize: '22px',
marginBottom: '10px',
textShadow: '2px 2px 4px #000',
borderBottom: '1px solid #555',
paddingBottom: '10px'
});
container.appendChild(title);
const itemsContainer = document.createElement('div');
Object.assign(itemsContainer.style, {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(80px, 1fr))',
gap: '10px',
maxHeight: '300px',
overflowY: 'auto',
padding: '5px'
});
container.appendChild(itemsContainer);
const inventory = hero.inventory || [];
if (inventory.length === 0) {
const emptyMsg = document.createElement('div');
emptyMsg.textContent = "La mochila está vacía...";
Object.assign(emptyMsg.style, {
textAlign: 'center',
color: '#888',
fontStyle: 'italic',
gridColumn: '1 / -1',
padding: '20px'
});
itemsContainer.appendChild(emptyMsg);
} else {
inventory.forEach((itemId, index) => {
const itemEl = this.createItemElement(itemId);
itemsContainer.appendChild(itemEl);
});
}
// Close button
const closeBtn = document.createElement('button');
closeBtn.textContent = 'Cerrar';
Object.assign(closeBtn.style, {
marginTop: '15px',
padding: '8px',
backgroundColor: '#444',
color: '#fff',
border: '1px solid #777',
borderRadius: '4px',
cursor: 'pointer',
fontFamily: '"Cinzel", serif'
});
closeBtn.onclick = () => this.toggle();
container.appendChild(closeBtn);
document.body.appendChild(container);
this.container = container;
}
createItemElement(itemId) {
const item = document.createElement('div');
Object.assign(item.style, {
width: '80px',
height: '80px',
backgroundColor: 'rgba(0, 0, 0, 0.6)',
border: '2px solid #DAA520',
borderRadius: '8px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
position: 'relative',
overflow: 'hidden'
});
const icon = document.createElement('div');
icon.style.fontSize = '32px';
const label = document.createElement('div');
label.style.fontSize = '10px';
label.style.textAlign = 'center';
label.style.marginTop = '4px';
// Item Database (Simple)
if (itemId === 'llave_rastrillo') {
icon.textContent = '🔑';
label.textContent = 'Llave Rastrillo';
} else {
icon.textContent = '📦';
label.textContent = itemId;
}
item.appendChild(icon);
item.appendChild(label);
item.onmouseenter = () => { item.style.backgroundColor = 'rgba(218, 165, 32, 0.2)'; };
item.onmouseleave = () => { item.style.backgroundColor = 'rgba(0, 0, 0, 0.6)'; };
return item;
}
}

View File

@@ -199,6 +199,9 @@ export class TurnStatusUI {
<div style="font-size: 14px;">
Moves: <span style="color: ${hero.currentMoves > 0 ? '#4f4' : '#f44'}; font-weight: bold;">${hero.currentMoves}</span> / ${hero.stats.move}
</div>
<div style="font-size: 14px;">
Oro: <span style="color: #DAA520; font-weight: bold;">${hero.stats.gold || 0}</span> 🪙
</div>
</div>
</div>
`;

View File

@@ -7,6 +7,7 @@ export class UnitCardManager {
this.cardsContainer = null;
this.currentHeroCard = null;
this.currentMonsterCard = null;
this.monsterContainer = null;
this.placeholderCard = null;
this.attackButton = null;
@@ -20,10 +21,10 @@ export class UnitCardManager {
left: '10px',
top: '220px', // Below minimap
display: 'flex',
flexDirection: 'column',
gap: '10px',
pointerEvents: 'auto',
width: '200px'
flexDirection: 'row',
alignItems: 'flex-start',
gap: '15px',
pointerEvents: 'auto'
});
this.parentContainer.appendChild(this.cardsContainer);
@@ -106,18 +107,18 @@ export class UnitCardManager {
const hero = this.game.heroes.find(h => h.id === heroId);
if (!hero) return;
const statsGrid = this.currentHeroCard.querySelector('div[style*="grid-template-columns"]');
if (statsGrid) {
const statDivs = statsGrid.children;
// Assumed order: 4 -> Heridas, 7 -> Movimiento
if (statDivs[4]) {
const wValue = statDivs[4].querySelector('span:last-child');
if (wValue) wValue.textContent = `${hero.currentWounds || hero.stats.wounds}/${hero.stats.wounds}`;
}
if (statDivs[7]) {
const movValue = statDivs[7].querySelector('span:last-child');
if (movValue) movValue.textContent = `${hero.currentMoves || 0}/${hero.stats.move}`;
}
// NEW: Update stats using data-attributes for robustness
const updateStat = (key, value) => {
const el = this.currentHeroCard.querySelector(`[data-stat="${key}"]`);
if (el) el.textContent = value;
};
updateStat('Her', `${hero.currentWounds || hero.stats.wounds}/${hero.stats.wounds}`);
updateStat('Mov', `${hero.currentMoves || 0}/${hero.stats.move}`);
updateStat('Oro', hero.stats.gold || 0);
if (hero.key === 'wizard') {
updateStat('Pod', hero.stats.power || 0);
}
}
@@ -191,15 +192,22 @@ export class UnitCardManager {
{ label: 'Her', value: `${hero.currentWounds || hero.stats.wounds}/${hero.stats.wounds}` },
{ label: 'Ini', value: hero.stats.initiative || 0 },
{ label: 'Ata', value: hero.stats.attacks || 0 },
{ label: 'Mov', value: `${hero.currentMoves || 0}/${hero.stats.move}` }
{ label: 'Mov', value: `${hero.currentMoves || 0}/${hero.stats.move}` },
{ label: 'Oro', value: hero.stats.gold || 0 }
];
// USER REQUEST: Show Power for Wizard
if (hero.key === 'wizard') {
stats.push({ label: 'Pod', value: hero.stats.power || 0 });
}
stats.forEach(stat => {
const el = document.createElement('div');
Object.assign(el.style, { backgroundColor: 'rgba(0, 0, 0, 0.5)', padding: '3px 5px', borderRadius: '3px', display: 'flex', justifyContent: 'space-between' });
const l = document.createElement('span'); l.textContent = stat.label + ':'; l.style.color = '#AAA';
const v = document.createElement('span'); v.textContent = stat.value; v.style.color = '#FFF'; v.style.fontWeight = 'bold';
v.dataset.stat = stat.label; // Add data attribute for easier updates
el.appendChild(l); el.appendChild(v);
statsGrid.appendChild(el);
@@ -280,11 +288,14 @@ export class UnitCardManager {
const invBtn = document.createElement('button');
invBtn.textContent = '🎒 INVENTARIO';
Object.assign(invBtn.style, {
width: '100%', padding: '8px', marginTop: '8px', backgroundColor: '#444',
color: '#fff', border: '1px solid #777', borderRadius: '4px',
fontFamily: '"Cinzel", serif', fontSize: '12px', cursor: 'pointer' // Changed cursor to pointer for feel, though functionality implies future
width: '100%', padding: '8px', marginTop: '8px', backgroundColor: '#5D4037',
color: '#fff', border: '1px solid #8B4513', borderRadius: '4px',
fontFamily: '"Cinzel", serif', fontSize: '12px', cursor: 'pointer'
});
invBtn.title = 'Inventario (Próximamente)';
invBtn.onclick = (e) => {
e.stopPropagation();
if (this.callbacks.toggleInventory) this.callbacks.toggleInventory(hero);
};
card.appendChild(invBtn);
@@ -365,8 +376,17 @@ export class UnitCardManager {
showMonsterCard(monster) {
this.hideMonsterCard();
// Create a sub-container for monster card + button
this.monsterContainer = document.createElement('div');
Object.assign(this.monsterContainer.style, {
display: 'flex',
flexDirection: 'column',
gap: '8px'
});
this.currentMonsterCard = this.createMonsterCard(monster);
this.cardsContainer.appendChild(this.currentMonsterCard);
this.monsterContainer.appendChild(this.currentMonsterCard);
this.attackButton = document.createElement('button');
this.attackButton.textContent = '⚔️ ATACAR';
@@ -384,7 +404,6 @@ export class UnitCardManager {
const result = this.game.performHeroAttack(monster.id);
if (result && result.success) {
this.hideMonsterCard();
// Optional: deselect monster logic if managed externally
if (this.game.selectedMonster) {
if (this.game.onEntitySelect) this.game.onEntitySelect(this.game.selectedMonster.id, false);
this.game.selectedMonster = null;
@@ -392,7 +411,8 @@ export class UnitCardManager {
}
}
};
this.cardsContainer.appendChild(this.attackButton);
this.monsterContainer.appendChild(this.attackButton);
this.cardsContainer.appendChild(this.monsterContainer);
}
showRangedAttackUI(monster) {
@@ -417,13 +437,11 @@ export class UnitCardManager {
}
hideMonsterCard() {
if (this.currentMonsterCard && this.currentMonsterCard.parentNode) {
this.cardsContainer.removeChild(this.currentMonsterCard);
this.currentMonsterCard = null;
if (this.monsterContainer && this.monsterContainer.parentNode) {
this.cardsContainer.removeChild(this.monsterContainer);
}
if (this.attackButton && this.attackButton.parentNode) {
this.cardsContainer.removeChild(this.attackButton);
this.monsterContainer = null;
this.currentMonsterCard = null;
this.attackButton = null;
}
}
}