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 = `
-${log.woundsCaused} HP
`; - } else { - detailHtml = `
Sin Heridas (Armadura)
`; - } - } else { - detailHtml = `
Esquivado / Fallado
`; - } - - // We create a new log element or update a singleton? - // The original logic updated a SINGLE notification area. - // Let's create a transient toast style log here, appending to container. - - const logItem = document.createElement('div'); - Object.assign(logItem.style, { - backgroundColor: 'rgba(0,0,0,0.9)', padding: '15px', border: `2px solid ${color}`, - borderRadius: '5px', textAlign: 'center', minWidth: '250px', marginBottom: '10px', - fontFamily: '"Cinzel", serif', opacity: '0', transition: 'opacity 0.3s' - }); - - logItem.innerHTML = ` -
${log.attackerId.split('_')[0]} ATACA
- ${detailHtml} -
${log.message}
- `; - - // Clear previous logs to act like the single notification area of before, OR stack them? - // Original behavior was overwrite `innerHTML`. I should stick to that to avoid spam. - // So I will clear `combatLogContainer` before adding. - this.combatLogContainer.innerHTML = ''; - this.combatLogContainer.appendChild(logItem); - - // Fade in - requestAnimationFrame(() => { logItem.style.opacity = '1'; }); - - // Fade out - setTimeout(() => { - logItem.style.opacity = '0'; - // We don't remove immediately to avoid layout jumps if another comes in, - // but we cleared logic above. - }, 3500); - } }