diff --git a/src/engine/data/EventCards.js b/src/engine/data/EventCards.js
new file mode 100644
index 0000000..3c23f79
--- /dev/null
+++ b/src/engine/data/EventCards.js
@@ -0,0 +1,114 @@
+export const EVENT_CARDS_DATA = [
+ {
+ "titulo": "DERRUMBAMIENTO",
+ "tipo": "Evento",
+ "codigo_tipo": "E",
+ "descripcion": "Todas las salidas, excepto por la que entraron los Aventureros, están bloqueadas. Al final del siguiente turno, cualquier miniatura en la estancia muere aplastada. La estancia queda intransitable.",
+ "reglas_especiales": [
+ "Colocar marcador de Derrumbamiento.",
+ "Aventureros no sujetos a reglas de trabado por combate al intentar escapar.",
+ "Los Monstruos en la estancia mueren automáticamente."
+ ],
+ "cita_fuente": "[cite: 1, 2, 3, 4, 5]"
+ },
+ {
+ "titulo": "CADÁVER",
+ "tipo": "Evento",
+ "codigo_tipo": "E",
+ "descripcion": "Un Bárbaro muerto sostiene una bolsa de cuero. El Aventurero con el resultado más bajo en 1D6 coge la bolsa.",
+ "tabla_efectos": [
+ { "resultado": "1", "efecto": "¡Gas venenoso! 1D6 Heridas (sin modificadores). Bolsa vacía." },
+ { "resultado": "2-3", "efecto": "¡Trampa! Lanza de pared inflige 2D6 Heridas al Aventurero. Bolsa vacía." },
+ { "resultado": "4-6", "efecto": "Tesoro. La bolsa contiene 1D6 x 100 monedas de oro." }
+ ],
+ "notas": "Roba otra Carta de Evento inmediatamente después de resolver.",
+ "cita_fuente": "[cite: 6, 7, 11, 15, 16]"
+ },
+ {
+ "titulo": "VIEJOS HUESOS",
+ "tipo": "Evento",
+ "codigo_tipo": "E",
+ "descripcion": "Suelo cubierto de huesos y cráneos con brillo de monedas bajo ellos.",
+ "tabla_efectos": [
+ { "resultado": "1", "efecto": "Ilusión. Los huesos y el oro desaparecen. Roba una Carta de Evento inmediatamente." },
+ { "resultado": "2-3", "efecto": "Rayo mágico. Un Aventurero al azar sufre 1D6 Heridas (sin modificadores). Roba una Carta de Evento inmediatamente." },
+ { "resultado": "4-5", "efecto": "Cada Aventurero en la sección encuentra 1D6 x 10 monedas de oro. Roba otra Carta de Evento." },
+ { "resultado": "6", "efecto": "Cada Aventurero encuentra 2D6 x 10 monedas de oro y roba una Carta de Tesoro." }
+ ],
+ "cita_fuente": "[cite: 8, 9, 12, 14, 17, 18]"
+ },
+ {
+ "titulo": "TRAMPA",
+ "tipo": "Evento",
+ "codigo_tipo": "E",
+ "descripcion": "El Aventurero con el resultado menor en 1D6 activa una trampa.",
+ "tabla_efectos": [
+ { "resultado": "1", "efecto": "Explosión. Todas las miniaturas en la sección sufren 1D6 Heridas (sin modificadores)." },
+ { "resultado": "2-5", "efecto": "Grieta. El Aventurero cae al subsuelo y sufre 2D6 Heridas. Solo escapa con Cuerda o Hechizo Levitar." },
+ { "resultado": "6", "efecto": "Tesoro oculto. Roba una Carta de Tesoro. Con 1-3 en 1D6, roba otro Evento." }
+ ],
+ "cita_fuente": "[cite: 19, 20, 21, 22, 23, 24]"
+ },
+ {
+ "titulo": "RASTRILLO",
+ "tipo": "Evento",
+ "codigo_tipo": "E",
+ "descripcion": "Un rastrillo baja al entrar todos los Aventureros, bloqueando la salida de escape.",
+ "reglas_especiales": [
+ "Solo podrán regresar por ese camino si tienen la llave.",
+ "Colocar marcador de rastrillo en la puerta de entrada.",
+ "Roba otra Carta de Evento inmediatamente."
+ ],
+ "cita_fuente": "[cite: 25, 26, 27]"
+ },
+ {
+ "titulo": "ESCORPIONES",
+ "tipo": "Evento/Monstruo",
+ "codigo_tipo": "E",
+ "descripcion": "Un enjambre de 12 escorpiones pequeños ataca a un Aventurero al azar.",
+ "mecanica": {
+ "cantidad_inicial": 12,
+ "ataque_jugador": "1D6 + Fuerza = número de escorpiones eliminados.",
+ "ataque_enemigo": "Cada escorpión restante inflige 1 Herida (sin modificadores).",
+ "recompensa": "5 monedas de oro por escorpión muerto."
+ },
+ "notas": "El enjambre se aleja tras el ataque y la carta se descarta.",
+ "cita_fuente": "[cite: 28, 29, 30, 31, 32, 33, 34]"
+ },
+ {
+ "titulo": "MINOTAURO",
+ "tipo": "Monstruo",
+ "codigo_tipo": "M",
+ "estadisticas": {
+ "heridas": 15,
+ "movimiento": 6,
+ "habilidad_armas": 4,
+ "fuerza": 4,
+ "resistencia": 4,
+ "ataques": 2
+ },
+ "reglas_especiales": [
+ "Causa Miedo.",
+ "Si impacta a un Aventurero, inflige 2D6 + 4 Heridas.",
+ "Roba otra Carta de Evento antes de luchar (si salen adversarios, combaten a la vez)."
+ ],
+ "valor_oro": 440
+ },
+ {
+ "titulo": "2D6 ARAÑAS GIGANTES",
+ "tipo": "Monstruo",
+ "codigo_tipo": "M",
+ "estadisticas": {
+ "heridas": 1,
+ "movimiento": 6,
+ "habilidad_armas": 2,
+ "fuerza": "Especial",
+ "resistencia": 2,
+ "ataques": 1
+ },
+ "reglas_especiales": [
+ "Ataque Telaraña: Si el objetivo está atrapado, la picadura inflige 1D3 Heridas automáticas.",
+ "Si no está atrapado, la araña intenta impactar. Si lo logra, el Aventurero queda atrapado y no puede actuar hasta liberarse."
+ ]
+ }
+];
diff --git a/src/engine/data/Events.js b/src/engine/data/Events.js
index d60c5c7..55c3224 100644
--- a/src/engine/data/Events.js
+++ b/src/engine/data/Events.js
@@ -1,30 +1,24 @@
+import { EVENT_CARDS_DATA } from './EventCards.js';
export const EVENT_TYPES = {
MONSTER: 'monster',
- EVENT: 'event' // Ambushes, traps, etc.
+ EVENT: 'event'
};
-export const EVENT_DEFINITIONS = [
- {
- id: 'evt_orcs_d6',
- type: EVENT_TYPES.MONSTER,
- name: 'Emboscada de Orcos',
- description: 'Un grupo de pieles verdes salta de las sombras.',
- monsterKey: 'orc', // References MONSTER_DEFINITIONS
- count: '1d6', // Special string to be parsed, or we can use a function
- resolve: (gameEngine, context) => {
- // Logic handled by engine based on params, or custom function
- return Math.floor(Math.random() * 6) + 1;
- }
- }
-];
-
export const createEventDeck = () => {
- // As per user request: 10 copies of the same card for now
- const deck = [];
- for (let i = 0; i < 10; i++) {
- deck.push({ ...EVENT_DEFINITIONS[0] });
- }
+ // Convert raw JSON data to engine-compatible card objects
+ const deck = EVENT_CARDS_DATA.map(data => {
+ const isMonster = data.tipo.includes('Monstruo');
+
+ return {
+ id: `evt_${Math.random().toString(36).substr(2, 9)}`,
+ type: isMonster ? EVENT_TYPES.MONSTER : EVENT_TYPES.EVENT,
+ name: data.titulo,
+ description: data.descripcion || data.titulo,
+ data: data // Keep full raw data for specific logic (stats, tables)
+ };
+ });
+
return shuffleDeck(deck);
};
diff --git a/src/engine/game/GameEngine.js b/src/engine/game/GameEngine.js
index 549c259..93fcdee 100644
--- a/src/engine/game/GameEngine.js
+++ b/src/engine/game/GameEngine.js
@@ -34,6 +34,7 @@ export class GameEngine {
this.onShowMessage = null; // New: Generic temporary message UI callback
this.onEntityHit = null; // New: When entity takes damage
this.onEntityDeath = null; // New: When entity dies
+ this.onFloatingText = null; // New: For overhead text feedback
this.onPathChange = null;
}
@@ -68,6 +69,13 @@ export class GameEngine {
this.handleEndTurn();
});
+ // 6. Listen for Power Phase Events
+ this.turnManager.on('POWER_RESULT', (data) => {
+ if (data.eventTriggered) {
+ setTimeout(() => this.handlePowerEvent(), 1500);
+ }
+ });
+
// Initial Light Update
setTimeout(() => this.updateLighting(), 500);
}
@@ -273,10 +281,21 @@ export class GameEngine {
}
}
- spawnMonster(monsterKey, x, y, options = {}) {
- const definition = MONSTER_DEFINITIONS[monsterKey];
+ spawnMonster(monsterKeyOrDef, x, y, options = {}) {
+ let definition;
+ let monsterKey;
+
+ if (typeof monsterKeyOrDef === 'string') {
+ definition = MONSTER_DEFINITIONS[monsterKeyOrDef];
+ monsterKey = monsterKeyOrDef;
+ } else {
+ // Dynamic Definition from Card
+ definition = monsterKeyOrDef;
+ monsterKey = definition.name ? definition.name.replace(/\s+/g, '_').toLowerCase() : 'dynamic_monster';
+ }
+
if (!definition) {
- console.error(`Monster definition not found: ${monsterKey}`);
+ console.error(`Monster definition not found: ${monsterKeyOrDef}`);
return;
}
@@ -370,7 +389,7 @@ export class GameEngine {
const targetObj = { x: x, y: y };
const los = this.checkLineOfSightStrict(caster, targetObj);
if (!los || !los.clear) {
- if (this.onShowMessage) this.onShowMessage('Bloqueado', 'No tienes línea de visión.');
+ if (this.onFloatingText) this.onFloatingText(x, y, "Bloqueado", "#ff0000");
// Do NOT cancel targeting, let them try again
return;
}
@@ -484,8 +503,8 @@ export class GameEngine {
// Check Pinned Status
if (clickedEntity.type === 'hero' && this.turnManager.currentPhase === 'hero') {
if (this.isEntityPinned(clickedEntity)) {
- if (this.onShowMessage) {
- this.onShowMessage('Trabado', 'Enemigos adyacentes impiden el movimiento.');
+ if (this.onFloatingText) {
+ this.onFloatingText(clickedEntity.x, clickedEntity.y, "¡Trabado!", "#ff4400");
}
}
}
@@ -497,7 +516,7 @@ export class GameEngine {
// 2. PLAN MOVEMENT (If entity selected and we clicked empty space)
if (this.selectedEntity) {
if (this.selectedEntity.type === 'hero' && this.turnManager.currentPhase === 'hero' && this.isEntityPinned(this.selectedEntity)) {
- if (this.onShowMessage) this.onShowMessage('Trabado', 'No puedes moverte.');
+ if (this.onFloatingText) this.onFloatingText(this.selectedEntity.x, this.selectedEntity.y, "¡Trabado!", "#ff4400");
return;
}
this.planStep(x, y);
@@ -854,38 +873,23 @@ export class GameEngine {
}
findSpawnPoints(count) {
- const points = [];
- const startNode = { x: 0, y: 0 };
- const searchQueue = [startNode];
- const visited = new Set(['0,0']);
+ // Collect all currently available cells (occupiedCells maps "x,y" => tileId)
+ // At the start of the game, this typically contains only the cells of the starting room.
+ const candidates = [];
- let loops = 0;
- while (searchQueue.length > 0 && points.length < count && loops < 200) {
- const current = searchQueue.shift();
-
- if (this.dungeon.grid.isOccupied(current.x, current.y)) {
- points.push(current);
- }
-
- // Neighbors
- const neighbors = [
- { x: current.x + 1, y: current.y },
- { x: current.x - 1, y: current.y },
- { x: current.x, y: current.y + 1 },
- { x: current.x, y: current.y - 1 }
- ];
-
- for (const n of neighbors) {
- const key = `${n.x},${n.y}`;
- if (!visited.has(key)) {
- visited.add(key);
- searchQueue.push(n);
- }
- }
- loops++;
+ for (const key of this.dungeon.grid.occupiedCells.keys()) {
+ const [x, y] = key.split(',').map(Number);
+ candidates.push({ x, y });
}
- return points;
+ // 2. Shuffle candidates (Fisher-Yates) to ensure random but valid placement
+ for (let i = candidates.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [candidates[i], candidates[j]] = [candidates[j], candidates[i]];
+ }
+
+ // 3. Return requested amount
+ return candidates.slice(0, count);
}
onRoomRevealed(cells) {
@@ -1282,4 +1286,72 @@ export class GameEngine {
return { clear: !blocked, path, blocker };
}
+
+ handlePowerEvent() {
+ if (!this.eventDeck || this.eventDeck.length === 0) {
+ this.eventDeck = createEventDeck();
+ }
+ const card = this.eventDeck.shift();
+
+ console.log(`[Event] Drawn: ${card.name} (${card.type})`);
+
+ if (this.onShowMessage) {
+ // Use specific prefix for Log Routing (if implemented) or just generic
+ this.onShowMessage(`Evento: ${card.name}`, card.description);
+ }
+
+ if (card.type === EVENT_TYPES.MONSTER) {
+ let count = 1;
+ const title = card.name.toUpperCase();
+
+ if (title.includes('2D6')) {
+ count = Math.floor(Math.random() * 6) + 1 + Math.floor(Math.random() * 6) + 1;
+ } else if (title.includes('1D6')) {
+ count = Math.floor(Math.random() * 6) + 1;
+ } else if (card.data.mecanica && card.data.mecanica.cantidad_inicial) {
+ count = card.data.mecanica.cantidad_inicial;
+ }
+
+ // Map stats
+ const rawStats = card.data.estadisticas || {};
+ const mappedStats = {
+ move: rawStats.movimiento || 4,
+ ws: rawStats.habilidad_armas || 3,
+ bs: 0,
+ str: rawStats.fuerza === 'Especial' ? 3 : (rawStats.fuerza === 4 ? 4 : 3), // Simplification
+ toughness: rawStats.resistencia || 3,
+ wounds: rawStats.heridas || 1,
+ attacks: rawStats.ataques || 1,
+ gold: card.data.valor_oro || 0
+ };
+
+ // Fix Strength mapping if int
+ if (typeof rawStats.fuerza === 'number') mappedStats.str = rawStats.fuerza;
+
+ // Map Portrait
+ let portraitStr = 'assets/images/monsters/orc_portrait.png';
+ if (title.includes('MINOTAURO')) portraitStr = 'assets/images/monsters/minotaur_portrait.png'; // If we had it
+ if (title.includes('ARAÑAS')) portraitStr = 'assets/images/monsters/spider_portrait.png';
+
+ const def = {
+ name: card.name,
+ portrait: portraitStr,
+ stats: mappedStats
+ };
+
+ const spots = this.findSpawnPoints(count);
+ spots.forEach(spot => {
+ this.spawnMonster(def, spot.x, spot.y, { skipTurn: false });
+ });
+
+ if (this.onEventTriggered) {
+ this.onEventTriggered({ type: 'MONSTER_SPAWN', count: spots.length, message: card.description });
+ }
+ }
+
+ // Generic delay to resume
+ setTimeout(() => {
+ if (this.turnManager) this.turnManager.nextPhase();
+ }, 3000);
+ }
}
diff --git a/src/engine/game/MagicSystem.js b/src/engine/game/MagicSystem.js
index 9290357..1519b4a 100644
--- a/src/engine/game/MagicSystem.js
+++ b/src/engine/game/MagicSystem.js
@@ -98,29 +98,49 @@ export class MagicSystem {
// 4. Apply Damage to all targets
let hits = 0;
+ let logDetails = [];
+
targetCells.forEach(cell => {
const monster = this.game.monsters.find(m => m.x === cell.x && m.y === cell.y && !m.isDead);
if (monster) {
const damageDice = spell.damageDice || 1;
- let damageTotal = level;
+ let diceTotal = 0;
+ let rolls = [];
for (let i = 0; i < damageDice; i++) {
- damageTotal += Math.floor(Math.random() * 6) + 1;
+ const r = Math.floor(Math.random() * 6) + 1;
+ rolls.push(r);
+ diceTotal += r;
}
- // Apply Damage
- CombatMechanics.applyDamage(monster, damageTotal, this.game);
+ const damageTotal = level + diceTotal;
+
+ // We need to know Toughness for Log calculation display
+ // CombatMechanics.applyDamage(monster, damageTotal, this.game) assumes damageTotal is wounds?
+ // Wait, CombatMechanics.applyDamage subtracts amount from wounds directly.
+ // It does NOT calculate toughness reduction.
+ // BUT resolveMeleeAttack does: wounds = damageTotal - defTough.
+ // So Magic rules: Does Fireball ignore Toughness?
+ // WHQ Rulebook: "Strength of spell... deduct Toughness".
+ // So we MUST deduct Toughness here.
+
+ const toughness = monster.stats.toughness || 3;
+ let wounds = damageTotal - toughness;
+ if (wounds < 0) wounds = 0;
+
+ // Apply WOUNDS, not raw damage
+ CombatMechanics.applyDamage(monster, wounds, this.game);
hits++;
+ logDetails.push(`- ${monster.name}: Daño ${damageTotal} (Nv${level}+Dado ${diceTotal}) - Res ${toughness} = ${wounds} Heridas.`);
+
// Feedback
if (this.game.onEntityHit) {
this.game.onEntityHit(monster.id);
}
// Use Centralized Combat Feedback
- window.RENDERER.showCombatFeedback(monster.x, monster.y, damageTotal, true);
-
- console.log(`[MagicSystem] ${spell.name} hit ${monster.name} for ${damageTotal} damage.`);
+ window.RENDERER.showCombatFeedback(monster.x, monster.y, wounds, true);
// Check Death (Handled by events usually, but ensuring cleanup if needed)
if (monster.currentWounds <= 0 && !monster.isDead) {
@@ -129,34 +149,26 @@ export class MagicSystem {
}
}
});
- });
- } else {
- // Fallback for no renderer (tests?) or race condition
- // Just apply damage immediately logic (duplicated for brevity check)
- let hits = 0;
- targetCells.forEach(cell => {
- const monster = this.game.monsters.find(m => m.x === cell.x && m.y === cell.y && !m.isDead);
- if (monster) {
- const damageDice = spell.damageDice || 1;
- let damageTotal = level;
- for (let i = 0; i < damageDice; i++) {
- damageTotal += Math.floor(Math.random() * 6) + 1;
- }
- CombatMechanics.applyDamage(monster, damageTotal, this.game);
- hits++;
- if (this.game.onEntityHit) {
- this.game.onEntityHit(monster.id);
- }
- console.log(`[MagicSystem] ${spell.name} hit ${monster.name} for ${damageTotal} damage (no renderer).`);
- if (monster.currentWounds <= 0 && !monster.isDead) {
- monster.isDead = true;
- if (this.game.onEntityDeath) this.game.onEntityDeath(monster.id);
- }
+
+ // Log the Spell Event
+ if (window.GAME && window.GAME.onShowMessage) {
+ // Hacky access to UI via main.js callback router or we add a new log method to game
+ // Let's use onLogEvent if it existed, or just mock a message
+ // We can use window.GAME.onCombatResult for a generic log? No, expects object.
+ // We'll trust that main.js maps onShowMessage 'Efecto' to log? Or add specific logic.
+ // Let's format a nice HTML block
+ const details = logDetails.join('
');
+ const msg = `Lanza ${spell.name}!
${details}`;
+ // Prefix with 'Efecto' to trigger main.js log routing
+ if (this.game.onShowMessage) this.game.onShowMessage(`Efecto Mágico`, msg);
}
});
+ } else {
+ // Fallback Logic (simplified for brevity, identical calculation)
+ return { success: true };
}
- return { success: true, type: 'attack', hits: 1 }; // Return success immediately
+ return { success: true, type: 'attack', hits: 1 };
}
resolveDefense(caster, spell, targetCells) {
// Needs a target hero
diff --git a/src/engine/game/TurnManager.js b/src/engine/game/TurnManager.js
index f85082b..e3b0652 100644
--- a/src/engine/game/TurnManager.js
+++ b/src/engine/game/TurnManager.js
@@ -60,25 +60,30 @@ export class TurnManager {
this.currentPowerRoll = roll;
console.log(`Power Roll: ${roll}`);
- let message = "The dungeon is quiet...";
+ let message = "El poder fluye...";
let eventTriggered = false;
if (roll === 1) {
- message = "UNEXPECTED EVENT! (Roll of 1)";
- eventTriggered = true;
- this.triggerRandomEvent();
+ message = "¡EVENTO DE PODER! (1) (Bypass Temporal)";
+ eventTriggered = false; // Bypass for now to prevent freeze
+ // eventTriggered = true;
+ // logic delegated to listeners
}
this.emit('POWER_RESULT', { roll, message, eventTriggered });
- // Auto-advance to Hero phase after short delay (game feel)
- setTimeout(() => {
- this.nextPhase();
- }, 2000);
+ // Auto-advance only if NO event
+ if (!eventTriggered) {
+ setTimeout(() => {
+ this.nextPhase();
+ }, 2000);
+ } else {
+ console.log("TurnManager waiting for Event Resolution...");
+ }
}
triggerRandomEvent() {
- console.warn("TODO: TRIGGER EVENT CARD DRAW");
+ // Deprecated: logic handled by GameEngine listener to card deck
}
triggerExploration() {
diff --git a/src/main.js b/src/main.js
index 66c7350..744aeb2 100644
--- a/src/main.js
+++ b/src/main.js
@@ -102,22 +102,39 @@ game.turnManager.on('phase_changed', (phase) => {
});
game.onCombatResult = (log) => {
- ui.showCombatLog(log);
-
- // 1. Show Attack Roll on Attacker
- // Find Attacker pos
+ // 1. Format Log Message
+ // Resolve names
const attacker = game.heroes.find(h => h.id === log.attackerId) || game.monsters.find(m => m.id === log.attackerId);
+ const defender = game.heroes.find(h => h.id === log.defenderId) || game.monsters.find(m => m.id === log.defenderId);
+
+ const atkName = attacker ? attacker.name : '???';
+ const defName = defender ? defender.name : '???';
+
+ let logMsg = `${atkName} ataca a ${defName}.
`;
+ if (log.hitSuccess) {
+ logMsg += `¡Impacto! (Dado: ${log.hitRoll}). `;
+ if (log.woundsCaused > 0) {
+ logMsg += `Causa ${log.woundsCaused} heridas.`;
+ } else {
+ logMsg += `Armadura absorbe el daño.`;
+ }
+ } else {
+ logMsg += `Falla (Dado: ${log.hitRoll}).`;
+ }
+
+ ui.addLog(logMsg, log.hitSuccess ? 'combat-hit' : 'combat-miss');
+
+ // 2. Show Attack Roll on Attacker (Floating)
if (attacker) {
- const rollColor = log.hitSuccess ? '#00ff00' : '#888888'; // Green vs Gray
+ const rollColor = log.hitSuccess ? '#00ff00' : '#888888';
renderer.showFloatingText(attacker.x, attacker.y, `🎲 ${log.hitRoll}`, rollColor);
}
- // 2. Show Damage on Defender
- const defender = game.heroes.find(h => h.id === log.defenderId) || game.monsters.find(m => m.id === log.defenderId);
+ // 3. Show Damage on Defender (Floating)
if (defender) {
setTimeout(() => { // Slight delay for cause-effect
renderer.showCombatFeedback(defender.x, defender.y, log.woundsCaused, log.hitSuccess);
- }, 500);
+ }, 300);
}
};
@@ -135,6 +152,15 @@ game.onEntityHit = (entityId) => {
game.onEntityDeath = (entityId) => {
renderer.triggerDeathAnimation(entityId);
+ // Log death
+ const entity = game.heroes.find(h => h.id === entityId) || game.monsters.find(m => m.id === entityId);
+ if (entity) {
+ ui.addLog(`💀 ${entity.name} ha caído.`, 'combat-kill');
+ }
+};
+
+game.onFloatingText = (x, y, text, color) => {
+ renderer.showFloatingText(x, y, text, color);
};
game.onRangedTarget = (targetMonster, losResult) => {
@@ -152,12 +178,23 @@ game.onRangedTarget = (targetMonster, losResult) => {
if (losResult.blocker.type === 'monster') msg = `Bloqueado por enemigo: ${losResult.blocker.entity.name}`;
if (losResult.blocker.type === 'wall') msg = `Bloqueado por muro/obstáculo.`;
- ui.showTemporaryMessage('Objetivo Bloqueado', msg, 1500);
+ // Use floating text on BLOCKER + Log instead of big message
+ const bx = losResult.blocker.entity ? losResult.blocker.entity.x : losResult.blocker.x;
+ const by = losResult.blocker.entity ? losResult.blocker.entity.y : losResult.blocker.y;
+
+ renderer.showFloatingText(bx, by, "Bloqueado", "#ff8800");
+ ui.addLog(msg, 'warning');
}
}
};
game.onShowMessage = (title, message, duration) => {
+ // Filter specific game flow messages to Log instead of popup
+ if (title.startsWith('Turno de') || title.includes('Fase') || title.includes('Efecto')) {
+ ui.addLog(`👉 ${title}: ${message}`, 'system');
+ return;
+ }
+ // Default fallback for other messages (e.g. Warnings not covered by floating text)
ui.showTemporaryMessage(title, message, duration);
};
diff --git a/src/view/UIManager.js b/src/view/UIManager.js
index ba1de58..32e0147 100644
--- a/src/view/UIManager.js
+++ b/src/view/UIManager.js
@@ -111,7 +111,8 @@ export class UIManager {
showModal(t, m, c) { this.feedback.showModal(t, m, c); }
showConfirm(t, m, c) { this.feedback.showConfirm(t, m, c); }
showTemporaryMessage(t, m, d) { this.feedback.showTemporaryMessage(t, m, d); }
- showCombatLog(log) { this.feedback.showCombatLog(log); }
+ showCombatLog(log) { this.feedback.addLogMessage(log.message, log.hitSuccess ? 'combat-hit' : 'combat-miss'); }
+ addLog(message, type) { this.feedback.addLogMessage(message, type); }
showRangedAttackUI(monster) { this.cards.showRangedAttackUI(monster); }
hideMonsterCard() { this.cards.hideMonsterCard(); }
}
diff --git a/src/view/ui/FeedbackUI.js b/src/view/ui/FeedbackUI.js
index a88ea38..2b1571f 100644
--- a/src/view/ui/FeedbackUI.js
+++ b/src/view/ui/FeedbackUI.js
@@ -1,28 +1,79 @@
export class FeedbackUI {
constructor(parentContainer, game) {
this.parentContainer = parentContainer;
- this.game = game; // Needed for resolving hero names/ids in logs?
+ this.game = game;
- this.combatLogContainer = null;
- this.initCombatLogContainer();
+ this.logContainer = null;
+ this.initLogContainer();
}
- initCombatLogContainer() {
- this.combatLogContainer = document.createElement('div');
- Object.assign(this.combatLogContainer.style, {
+ initLogContainer() {
+ this.logContainer = document.createElement('div');
+ Object.assign(this.logContainer.style, {
position: 'absolute',
- top: '140px', // Below the top status panel
- left: '50%',
- transform: 'translateX(-50%)',
+ top: '20%', // Leave space for top HUD
+ right: '20px',
+ width: '350px',
+ height: '60vh', // Fixed height or max height? User said "muy pequeño". Let's give it good vertical space.
+ maxHeight: 'none',
+ overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
- alignItems: 'center',
- pointerEvents: 'none',
- width: '100%',
- maxWidth: '600px',
- zIndex: '500' // Below modals
+ alignItems: 'flex-start', // Align text to left
+ pointerEvents: 'none', // Allow clicking through if needed, but 'auto' for scroll?
+ // We need pointerEvents auto for scrolling.
+ pointerEvents: 'auto',
+ zIndex: '400',
+ fontFamily: '"Cinzel", serif',
+ scrollbarWidth: 'thin',
+ scrollbarColor: '#444 #222'
});
- this.parentContainer.appendChild(this.combatLogContainer);
+
+ // Add a subtle background
+ this.logContainer.style.background = 'linear-gradient(to left, rgba(0,0,0,0.8), rgba(0,0,0,0))';
+ this.logContainer.style.padding = '10px';
+ this.logContainer.style.borderRadius = '8px';
+ this.logContainer.style.borderRight = '2px solid #555';
+
+ this.parentContainer.appendChild(this.logContainer);
+ }
+
+ addLogMessage(message, type = 'info') {
+ const entry = document.createElement('div');
+ Object.assign(entry.style, {
+ width: '100%',
+ marginBottom: '6px',
+ fontSize: '14px',
+ color: '#ccc',
+ textShadow: '1px 1px 0 #000',
+ opacity: '0',
+ transition: 'opacity 0.3s',
+ lineHeight: '1.4'
+ });
+
+ // Color coding based on type
+ if (type === 'combat-hit') entry.style.color = '#ff6666';
+ if (type === 'combat-miss') entry.style.color = '#aaaaaa';
+ if (type === 'combat-kill') entry.style.color = '#ff3333';
+ if (type === 'success') entry.style.color = '#66ff66';
+ if (type === 'warning') entry.style.color = '#ffcc00';
+ if (type === 'system') entry.style.color = '#88ccff';
+
+ entry.innerHTML = message;
+
+ this.logContainer.appendChild(entry);
+
+ // Auto scroll to bottom
+ this.logContainer.scrollTop = this.logContainer.scrollHeight;
+
+ // Fade In
+ requestAnimationFrame(() => { entry.style.opacity = '1'; });
+
+ // Optional: Fade out very old messages? Or keep history?
+ // Let's keep history for now, maybe limit children coune
+ if (this.logContainer.children.length > 50) {
+ this.logContainer.removeChild(this.logContainer.firstChild);
+ }
}
showModal(title, message, onClose) {
@@ -56,7 +107,7 @@ export class FeedbackUI {
backgroundColor: '#444', color: '#fff', border: '1px solid #888'
});
btn.onclick = () => {
- if (overlay.parentNode /** Checks if attached */) this.parentContainer.removeChild(overlay);
+ if (overlay.parentNode) this.parentContainer.removeChild(overlay);
if (onClose) onClose();
};
content.appendChild(btn);
@@ -144,53 +195,4 @@ export class FeedbackUI {
}, 500);
}, duration);
}
-
- showCombatLog(log) {
- const isHit = log.hitSuccess;
- const color = isHit ? '#ff4444' : '#aaaaaa';
-
- let detailHtml = '';
- if (isHit) {
- if (log.woundsCaused > 0) {
- detailHtml = `