348 lines
13 KiB
JavaScript
348 lines
13 KiB
JavaScript
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 <b>${names}</b> ${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", `<b>${targetName}</b> tira <b>${action.tipo_prueba}</b>: Resultado <b style="color:#DAA520">${roll}</b>`);
|
|
|
|
// 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", `<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 => {
|
|
h.stats.gold = (h.stats.gold || 0) + amount;
|
|
if (this.game.onEntityUpdate) this.game.onEntityUpdate(h);
|
|
});
|
|
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) {
|
|
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 <b>${count} ${def.name}</b>!<br>¡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!<br><b>${collapsedCount}</b> 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";
|
|
}
|
|
}
|