Fix(GameEngine): Critical Room Reveal hang resolve and Devlog update

This commit is contained in:
2026-01-10 00:18:43 +01:00
parent e22cd071c4
commit 82bdcacf95
18 changed files with 1117 additions and 195 deletions

View File

@@ -0,0 +1,380 @@
/**
* EventInterpreter.js
*
* Takes high-level Action Instructions from Event Cards (JSON)
* and executes them using the Game Engine's low-level systems.
*/
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 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 {
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;
// Next step
if (this.queue.length > 0) {
setTimeout(() => this.processQueue(onComplete), 500);
} else {
this.processQueue(onComplete); // Finish up
}
}
async executeAction(action) {
switch (action.tipo_accion) {
case 'MENSAJE':
await this.showModal("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') {
// 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];
} else if (roll === lowest) {
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
}
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>`);
}
}
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);
return;
}
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>`);
// 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);
if (roll >= min && roll <= max) resultActions = action.tabla[key];
} else {
if (roll === Number(key)) resultActions = action.tabla[key];
}
}
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;
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 (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
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(", ")}`);
} else if (action.tipo === 'oro') {
targets.forEach(h => {
console.log(`Giving ${amount} Gold to ${h.name}`);
h.gold = (h.gold || 0) + amount;
if (this.game.onEntityUpdate) this.game.onEntityUpdate(h);
});
await this.showModal("Oro", `${msg}<br>Ganan <b>${amount}</b> de Oro.`);
}
}
async handleSpawn(action) {
// Parse Amount
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;
}
// 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 }
};
// 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.
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.");
}
} 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 {
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}`;
}
}