import { MONSTER_DEFINITIONS } from '../data/Monsters.js'; import { CombatMechanics } from '../game/CombatMechanics.js'; export class EventInterpreter { constructor(gameEngine) { this.game = gameEngine; this.queue = []; this.isProcessing = false; this.currentContext = {}; // Store temporary variables (e.g. "victim") } /** * Entry point to execute a card's actions * @param {Object} card The event card data */ async processEvent(card, onComplete = null) { if (!card.acciones || !Array.isArray(card.acciones)) { console.warn("[EventInterpreter] Card has no actions:", card); if (onComplete) onComplete(); return; } console.log(`[EventInterpreter] Processing ${card.titulo}`); // Log Entry (System Trace) if (this.game.onShowMessage) { this.game.onShowMessage("Evento de Poder", `${card.titulo}`); } // STEP 1: Show the Card to the User await this.showEventCard(card); // Load actions into queue this.queue = [...card.acciones]; this.processQueue(onComplete); } // Helper wrapper for async UI async showEventCard(card) { return new Promise(resolve => { if (this.game.onShowEvent) { this.game.onShowEvent(card, resolve); } else { console.warn("[EventInterpreter] onShowEvent not bound, auto-resolving"); resolve(); } }); } async showModal(title, text) { return new Promise(resolve => { // Use GameEngine's generic message system IF it supports callback-based waiting // Currently onShowMessage is usually non-blocking temporary. // We need a blocking Modal. // Let's assume onShowEvent can be reused or we use a specific one. // For now, let's reuse onShowEvent with a generic structure or define a new callback. // Actually, we can just construct a mini-card object for onShowEvent const miniCard = { titulo: title, texto: text, tipo: 'Resolución' }; if (this.game.onShowEvent) { this.game.onShowEvent(miniCard, resolve); } else { resolve(); } }); } async 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."); if (onComplete) { onComplete(); } else { this.game.turnManager.resumeFromEvent(); // Resume turn flow } return; } this.isProcessing = true; const action = this.queue.shift(); try { await this.executeAction(action); } catch (error) { console.error("[EventInterpreter] Error executing action:", error); } this.isProcessing = false; // Increased delay to 2s between major event steps as requested if (this.queue.length > 0) { setTimeout(() => this.processQueue(onComplete), 2000); } else { this.processQueue(onComplete); // Finish up } } async executeAction(action) { switch (action.tipo_accion) { case 'MENSAJE': await this.log("Evento", action.texto || action.mensaje); break; case 'SELECCION': await this.handleSelection(action); break; case 'PRUEBA': await this.handleTest(action); break; case 'EFECTO': await this.handleEffect(action); break; case 'SPAWN': await this.handleSpawn(action); break; case 'ENTORNO': await this.handleEnvironment(action); break; case 'ESTADO_GLOBAL': if (!this.game.state) this.game.state = {}; this.game.state[action.clave] = action.valor; console.log(`[Event] Global State Update: ${action.clave} = ${action.valor}`); break; case 'TRIGGER_EVENTO': console.log("TODO: Chain Event Draw"); break; default: console.warn("Unknown Action:", action.tipo_accion); } } async handleSelection(action) { let targets = []; if (action.modo === 'todos') { targets = [...this.game.heroes]; } else if (action.modo === 'azar') { const idx = Math.floor(Math.random() * this.game.heroes.length); targets = [this.game.heroes[idx]]; } else if (action.modo === 'tirada_baja') { let lowest = 99; let candidates = []; this.game.heroes.forEach(h => { const roll = Math.floor(Math.random() * 6) + 1; if (roll < lowest) { lowest = roll; candidates = [h]; } else if (roll === lowest) { candidates.push(h); // Tie } }); targets = [candidates[Math.floor(Math.random() * candidates.length)]]; } // Store result if (action.guardar_como) { this.currentContext[action.guardar_como] = targets[0]; } if (action.mensaje) { const names = targets.map(t => t.name).join(", "); // Improved immersive message: "¡El Bárbaro ha pisado una trampa!" await this.log("Selección", `¡El ${names} ${action.mensaje}`); } } async handleTest(action) { const target = action.origen ? this.currentContext[action.origen] : null; if (!target && action.origen) { console.error("Test target not found in context:", action.origen); return; } let roll = 0; 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; } // 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; for (const key of Object.keys(action.tabla)) { if (key.includes('-')) { const [min, max] = key.split('-').map(Number); if (roll >= min && roll <= max) resultActions = action.tabla[key]; } else { if (roll === Number(key)) resultActions = action.tabla[key]; } } if (resultActions) { this.queue.unshift(...resultActions); } } } async handleEffect(action) { let targets = []; if (action.objetivo) { if (this.currentContext[action.objetivo]) targets = [this.currentContext[action.objetivo]]; } let amount = 0; let isDice = false; if (typeof action.cantidad === 'number') amount = action.cantidad; else if (typeof action.cantidad === 'string' && action.cantidad.includes('D6')) { isDice = true; const parts = action.cantidad.split('*'); const dicePart = parts[0]; const multiplier = parts[1] ? parseInt(parts[1]) : 1; let roll = 0; const numDice = parseInt(dicePart) || 1; for (let i = 0; i < numDice; i++) roll += Math.floor(Math.random() * 6) + 1; amount = roll * multiplier; } let msg = action.mensaje || "Efecto aplicado"; if (action.tipo === 'daño') { targets.forEach(h => { // 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); }); // 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 => { h.stats.gold = (h.stats.gold || 0) + amount; if (this.game.onEntityUpdate) this.game.onEntityUpdate(h); }); 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) { let count = 1; if (typeof action.cantidad === 'string' && action.cantidad.includes('D6')) { const numDice = parseInt(action.cantidad) || 1; for (let i = 0; i < numDice; i++) count += Math.floor(Math.random() * 6) + 1; } else { count = parseInt(action.cantidad) || 1; } 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 } }; } 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 => { this.game.spawnMonster(def, spot.x, spot.y, { skipTurn: false }); }); // 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') { if (this.game.collapseExits) { const collapsedCount = this.game.collapseExits(); await this.showModal("¡Derrumbe!", `¡El techo se viene abajo!
${collapsedCount} salidas bloqueadas.`); } } else if (action.subtipo === 'colocar_marcador') { if (this.game.placeEventMarker) { 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); } } // Legacy fallback ONLY getTexturePathForMonster_Legacy(id) { return "assets/images/dungeon1/standees/enemies/orc.png"; } }