Sesión 15: Implementación de Inventario y lógica de evento de Rastrillo/Llave
This commit is contained in:
@@ -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 = {
|
||||
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 <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";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user