diff --git a/DEVLOG.md b/DEVLOG.md index 015aaec..c6279fa 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -379,3 +379,45 @@ Establecimiento de la base completa del motor de juego con generación procedime - ✅ Visualización 3D con Three.js - ✅ Sistema de cámara isométrica - ✅ Carga de texturas y assets + +## Sesión 9: Pulido de Combate, UI de Hechizos y Buffs +**Fecha:** 8 de Enero de 2026 + +### Objetivos +- Resolver la duplicación de animaciones en el ataque de los monstruos. +- Mejorar la interfaz de usuario para el manejo de hechizos (Libro de Hechizos Visual). +- Implementar validaciones de línea de visión (LOS) en el lanzamiento de hechizos. +- Añadir nuevos hechizos ("Piel de Hierro") y sistema de duración de efectos (Buffs). + +### Cambios Realizados + +#### 1. Corrección de Animaciones y Audio +- **Doble Animación**: Se eliminó la llamada redundante a `onEntityHit` dentro de `MonsterAI.js`. Ahora el feedback visual (destello rojo/temblor) se delega exclusivamente a `game.onCombatResult`, unificando el flujo entre héroes y monstruos y evitando que la animación se dispare dos veces. +- **Audio**: Se investigó el retraso en el audio del golpe. Se decidió mantener el sonido actual (`sword1.mp3`) por el momento. + +#### 2. Interfaz de Usuario (UI) +- **Botón de Inventario**: Añadido un botón placeholder "🎒 INVENTARIO" a las fichas de todos los aventureros. +- **Libro de Hechizos (Mago)**: + - Se reemplazó la lista de botones de texto por un sistema visual de cartas. + - Al hacer clic en "HECHIZOS", se despliega una mano de cartas generadas dinámicamente con plantillas (`attack_template`, `defense_template`, `healing_template`). + - Las cartas muestran el coste de poder en la esquina y se oscurecen si no hay maná suficiente. + - Implementado cierre automático al seleccionar o hacer clic fuera. + +#### 3. Sistema de Magia y Buffs +- **Validación LOS**: Corregido bug donde "Bola de Fuego" podía lanzarse a través de muros aunque la previsualización mostrara rojo. Ahora `onCellClick` valida estrictamente la línea de visión antes de ejecutar. +- **Nuevo Hechizo: Piel de Hierro**: + - Coste: 5. Tipo: Defensa. + - Efecto: Otorga +2 a Resistencia durante 1 turno. + - Requiere selección de objetivo (héroe). +- **Sistema de Buffs Temporales**: + - Implementado evento `turn_ended` en `TurnManager`. + - Añadido método `handleEndTurn` en `GameEngine` para gestionar la duración de los efectos. + - Los buffs ahora se limpian automáticamente cuando su duración llega a 0, revirtiendo los cambios en las estadísticas. + +### Estado Actual +El combate se siente mucho más sólido sin las animaciones dobles. La interfaz del mago es ahora visualmente atractiva y funcional. El sistema de magia soporta hechizos de defensa y buffs con duración limitada, abriendo la puerta a mecánicas más complejas. + +### Próximos Pasos +- Implementar la funcionalidad real del Inventario. +- Añadir más cartas/hechizos y refinar el diseño visual de los textos en las cartas. +- Ajustar el timing del sonido de ataque para sincronizarlo perfectamente con la animación de impacto. diff --git a/public/assets/images/dungeon1/spells/attack_template.png b/public/assets/images/dungeon1/spells/attack_template.png new file mode 100644 index 0000000..c675145 Binary files /dev/null and b/public/assets/images/dungeon1/spells/attack_template.png differ diff --git a/public/assets/images/dungeon1/spells/defense_template.png b/public/assets/images/dungeon1/spells/defense_template.png new file mode 100644 index 0000000..8a887ac Binary files /dev/null and b/public/assets/images/dungeon1/spells/defense_template.png differ diff --git a/public/assets/images/dungeon1/spells/healing_template.png b/public/assets/images/dungeon1/spells/healing_template.png new file mode 100644 index 0000000..3f31633 Binary files /dev/null and b/public/assets/images/dungeon1/spells/healing_template.png differ diff --git a/src/engine/data/Spells.js b/src/engine/data/Spells.js index ce5a109..d1be16d 100644 --- a/src/engine/data/Spells.js +++ b/src/engine/data/Spells.js @@ -4,18 +4,32 @@ export const SPELLS = [ id: 'fireball', name: 'Bola de Fuego', type: 'attack', - cost: 1, + cost: 5, range: 12, // Arbitrary line of sight damageDice: 1, damageBonus: 'hero_level', // Dynamic logic area: 2, // 2x2 description: "Elige un área de 2x2 casillas en línea de visión. Cada miniatura sufre 1D6 + Nivel herois." }, + { + id: 'iron_skin', + name: 'Piel de Hierro', + type: 'defense', + cost: 1, + range: 'board', // Anywhere on board + target: 'single_hero', // Needs selection + effect: { + stat: 'toughness', + value: 2, + duration: 1 + }, + description: "Elige a un Aventurero. +2 a Resistencia durante este turno." + }, { id: 'healing_hands', name: 'Manos Curadoras', type: 'heal', - cost: 1, + cost: 2, range: 'board', // Same board section healAmount: 1, target: 'all_heroes', diff --git a/src/engine/game/GameEngine.js b/src/engine/game/GameEngine.js index 60936c0..e59b4c4 100644 --- a/src/engine/game/GameEngine.js +++ b/src/engine/game/GameEngine.js @@ -53,6 +53,11 @@ export class GameEngine { this.resetHeroMoves(); } }); + + // End of Turn Logic (Buffs, cooldowns, etc) + this.turnManager.on('turn_ended', (turn) => { + this.handleEndTurn(); + }); } resetHeroMoves() { @@ -64,6 +69,49 @@ export class GameEngine { console.log("Refilled Hero Moves"); } + handleEndTurn() { + console.log("[GameEngine] Handling End of Turn Effects..."); + + if (!this.heroes) return; + + this.heroes.forEach(hero => { + if (hero.buffs && hero.buffs.length > 0) { + // Decrement duration + hero.buffs.forEach(buff => { + buff.duration--; + }); + + // Remove expired + const activeBuffs = []; + const expiredBuffs = []; + + hero.buffs.forEach(buff => { + if (buff.duration > 0) { + activeBuffs.push(buff); + } else { + expiredBuffs.push(buff); + } + }); + + // Revert expired + expiredBuffs.forEach(buff => { + if (buff.stat === 'toughness') { + hero.stats.toughness -= buff.value; + if (hero.tempStats && hero.tempStats.toughnessBonus) { + hero.tempStats.toughnessBonus -= buff.value; + } + console.log(`[GameEngine] Buff expired: ${buff.id} on ${hero.name}. -${buff.value} ${buff.stat}`); + if (this.onShowMessage) { + this.onShowMessage("Efecto Finalizado", `La ${buff.id === 'iron_skin' ? 'Piel de Hierro' : 'Magia'} de ${hero.name} se desvanece.`); + } + } + }); + + hero.buffs = activeBuffs; + } + }); + } + createParty() { this.heroes = []; this.monsters = []; // Initialize monsters array @@ -204,17 +252,32 @@ export class GameEngine { targetCells.push({ x: x, y: y }); } + // NEW: Enforce LOS Check before execution + const caster = this.selectedEntity; + if (caster) { + 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.'); + // Do NOT cancel targeting, let them try again + return; + } + } + // Execute Spell const result = this.executeSpell(this.currentSpell, targetCells); if (result.success) { // Success + this.cancelTargeting(); + if (window.RENDERER) window.RENDERER.hideAreaPreview(); } else { if (this.onShowMessage) this.onShowMessage('Fallo', result.reason || 'No se pudo lanzar el hechizo.'); + this.cancelTargeting(); // Cancel on error? maybe keep open? usually cancel. + if (window.RENDERER) window.RENDERER.hideAreaPreview(); } - this.cancelTargeting(); - if (window.RENDERER) window.RENDERER.hideAreaPreview(); + return; } diff --git a/src/engine/game/MagicSystem.js b/src/engine/game/MagicSystem.js index 4c17133..9290357 100644 --- a/src/engine/game/MagicSystem.js +++ b/src/engine/game/MagicSystem.js @@ -36,6 +36,8 @@ export class MagicSystem { return this.resolveHeal(caster, spell); } else if (spell.type === 'attack') { return this.resolveAttack(caster, spell, targetCells); + } else if (spell.type === 'defense') { + return this.resolveDefense(caster, spell, targetCells); } return { success: false, reason: 'unknown_spell_type' }; @@ -156,4 +158,67 @@ export class MagicSystem { return { success: true, type: 'attack', hits: 1 }; // Return success immediately } + resolveDefense(caster, spell, targetCells) { + // Needs a target hero + let targetHero = null; + + // Find hero in target cells + for (const cell of targetCells) { + const h = this.game.heroes.find(h => h.x === cell.x && h.y === cell.y); + if (h) { + targetHero = h; + break; + } + } + + if (!targetHero) { + return { success: false, reason: 'no_target_hero' }; + } + + const effect = spell.effect; + if (!effect) return { success: false }; + + // Apply Buff + if (effect.stat === 'toughness') { + // Store original if not already stored (simple buffering) + if (!targetHero.tempStats) targetHero.tempStats = {}; + + // Stackable? Probably not for same spell. + // Check if already has this buff? + // For simplicity: Add modifier + if (!targetHero.tempStats.toughnessBonus) targetHero.tempStats.toughnessBonus = 0; + + targetHero.tempStats.toughnessBonus += effect.value; + // Also modify actual stat for calculation access? + // Usually stats are accessed via getter or direct. + // If direct property, we modify it and store original? + // Let's modify the stat directly for now and trust Turn Manager to revert or track it. + // BETTER: modify 'toughness' in stats, store 'buff_iron_skin' in activeBuffs? + + targetHero.stats.toughness += effect.value; + + // Mark for cleanup (Pseudo-implementation for cleanup) + if (!targetHero.buffs) targetHero.buffs = []; + targetHero.buffs.push({ + id: spell.id, + stat: 'toughness', + value: effect.value, + duration: effect.duration + }); + + if (this.game.onShowMessage) { + this.game.onShowMessage('Piel de Hierro', `Resistencia de ${targetHero.name} +${effect.value}`); + } + + // Visual Effect + if (window.RENDERER) { + window.RENDERER.triggerVisualEffect('defense_buff', targetHero.x, targetHero.y); + // Highlight or keep aura? + } + + console.log(`[MagicSystem] Applied ${spell.name} to ${targetHero.name}`); + } + + return { success: true, type: 'defense', target: targetHero.name }; + } } diff --git a/src/engine/game/MonsterAI.js b/src/engine/game/MonsterAI.js index 5daf4c7..197ace4 100644 --- a/src/engine/game/MonsterAI.js +++ b/src/engine/game/MonsterAI.js @@ -230,9 +230,8 @@ export class MonsterAI { // Step 2: Attack animation delay (500ms) setTimeout(() => { // Step 3: Trigger hit visual on defender (if hit succeeded) - if (result.hitSuccess && this.game.onEntityHit) { - this.game.onEntityHit(hero.id); - } + // Step 3: Trigger hit visual on defender REMOVED (Handled by onCombatResult) + // Step 4: Remove green ring after red ring appears (1200ms for red ring duration) setTimeout(() => { diff --git a/src/engine/game/TurnManager.js b/src/engine/game/TurnManager.js index ef5a130..f85082b 100644 --- a/src/engine/game/TurnManager.js +++ b/src/engine/game/TurnManager.js @@ -88,6 +88,7 @@ export class TurnManager { endTurn() { console.log(`--- TURN ${this.currentTurn} END ---`); + this.emit('turn_ended', this.currentTurn); this.currentTurn++; this.startPowerPhase(); } diff --git a/src/view/UIManager.js b/src/view/UIManager.js index 8e63609..d8d65e4 100644 --- a/src/view/UIManager.js +++ b/src/view/UIManager.js @@ -600,50 +600,45 @@ export class UIManager { }; } card.appendChild(bowBtn); - } else if (hero.key === 'wizard') { - // SPELLS UI - const spellsTitle = document.createElement('div'); - spellsTitle.textContent = "HECHIZOS (Poder: " + (this.game.turnManager.power || 0) + ")"; - spellsTitle.style.marginTop = '10px'; - spellsTitle.style.fontSize = '12px'; - spellsTitle.style.fontWeight = 'bold'; - spellsTitle.style.color = '#aa88ff'; - spellsTitle.style.borderBottom = '1px solid #aa88ff'; - card.appendChild(spellsTitle); + } - SPELLS.forEach(spell => { - const btn = document.createElement('button'); - btn.textContent = `${spell.name} (${spell.cost})`; - btn.title = spell.description; - btn.style.width = '100%'; - btn.style.padding = '5px'; - btn.style.marginTop = '4px'; - btn.style.fontSize = '11px'; - btn.style.fontFamily = '"Cinzel", serif'; + // INVENTORY BUTTON (For All Heroes) + const invBtn = document.createElement('button'); + invBtn.textContent = '🎒 INVENTARIO'; + invBtn.style.width = '100%'; + invBtn.style.padding = '8px'; + invBtn.style.marginTop = '8px'; + invBtn.style.backgroundColor = '#444'; + invBtn.style.color = '#fff'; + invBtn.style.border = '1px solid #777'; + invBtn.style.borderRadius = '4px'; + invBtn.style.fontFamily = '"Cinzel", serif'; + invBtn.style.fontSize = '12px'; + invBtn.style.cursor = 'not-allowed'; // Placeholder functionality + invBtn.title = 'Inventario (Próximamente)'; + card.appendChild(invBtn); - const canCast = this.game.canCastSpell(spell); + // SPELLS UI (Wizard Only) + if (hero.key === 'wizard') { + const spellsBtn = document.createElement('button'); + spellsBtn.textContent = '🔮 HECHIZOS'; + spellsBtn.style.width = '100%'; + spellsBtn.style.padding = '8px'; + spellsBtn.style.marginTop = '5px'; + spellsBtn.style.backgroundColor = '#4b0082'; // Indigo + spellsBtn.style.color = '#fff'; + spellsBtn.style.border = '1px solid #8a2be2'; + spellsBtn.style.borderRadius = '4px'; + spellsBtn.style.fontFamily = '"Cinzel", serif'; + spellsBtn.style.cursor = 'pointer'; - btn.style.backgroundColor = canCast ? '#4b0082' : '#333'; - btn.style.color = canCast ? '#fff' : '#888'; - btn.style.border = '1px solid #666'; - btn.style.cursor = canCast ? 'pointer' : 'not-allowed'; + spellsBtn.onclick = (e) => { + e.stopPropagation(); + // Toggle Spell Book UI + this.toggleSpellBook(hero); + }; - if (canCast) { - btn.onclick = (e) => { - e.stopPropagation(); - - if (spell.type === 'attack') { - // Use Targeting Mode - this.game.startSpellTargeting(spell); - } else { - // Healing is instant/global (no target needed for 'healing_hands') - this.game.executeSpell(spell); - } - }; - } - - card.appendChild(btn); - }); + card.appendChild(spellsBtn); } card.dataset.heroId = hero.id; @@ -1381,4 +1376,165 @@ export class UIManager { }, 500); }, duration); } + toggleSpellBook(hero) { + // Close if already open + if (this.spellBookContainer) { + document.body.removeChild(this.spellBookContainer); + this.spellBookContainer = null; + return; + } + + // Create Container + const container = document.createElement('div'); + Object.assign(container.style, { + position: 'absolute', + bottom: '140px', // Just above placement/UI bottom area + left: '50%', + transform: 'translateX(-50%)', + display: 'flex', + gap: '15px', + backgroundColor: 'rgba(20, 10, 30, 0.9)', + padding: '20px', + borderRadius: '10px', + border: '2px solid #9933ff', + zIndex: '1500', + boxShadow: '0 0 20px rgba(100, 0, 255, 0.5)' + }); + + // Title + const title = document.createElement('div'); + title.textContent = "LIBRO DE HECHIZOS"; + Object.assign(title.style, { + position: 'absolute', + top: '-30px', + left: '0', + width: '100%', + textAlign: 'center', + color: '#d8bfff', + fontFamily: '"Cinzel", serif', + fontSize: '18px', + textShadow: '0 0 5px #8a2be2' + }); + container.appendChild(title); + + const currentPower = this.game.turnManager.power || 0; + + // Render Spells as Cards + SPELLS.forEach(spell => { + const canCast = this.game.canCastSpell(spell); + + const card = document.createElement('div'); + Object.assign(card.style, { + width: '180px', + height: '260px', + position: 'relative', + cursor: canCast ? 'pointer' : 'not-allowed', + transition: 'transform 0.2s', + filter: canCast ? 'none' : 'grayscale(100%) brightness(50%)', + backgroundImage: this.getSpellTemplate(spell.type), + backgroundSize: 'cover' + }); + + if (canCast) { + card.onmouseenter = () => { card.style.transform = 'scale(1.1) translateY(-10px)'; card.style.zIndex = '10'; }; + card.onmouseleave = () => { card.style.transform = 'scale(1)'; card.style.zIndex = '1'; }; + + card.onclick = (e) => { + e.stopPropagation(); + // Close book + document.body.removeChild(this.spellBookContainer); + this.spellBookContainer = null; + + if (spell.type === 'attack' || spell.type === 'defense') { + // Use Targeting Mode for attacks AND defense (buffs on allies) + this.game.startSpellTargeting(spell); + } else { + // Global/Instant spells (like healing_hands currently configured as global in previous logic, though typically healing hands is touch... let's keep it consistent with previous logic for now) + this.game.executeSpell(spell); + } + }; + } + + // COST INDICATOR (Top Left) + const costBadge = document.createElement('div'); + costBadge.textContent = spell.cost; + Object.assign(costBadge.style, { + position: 'absolute', + top: '12px', + left: '12px', + width: '30px', + height: '30px', + borderRadius: '50%', + backgroundColor: '#fff', + color: '#000', + fontWeight: 'bold', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + border: '2px solid #000', + fontSize: '18px', + fontFamily: 'serif' + }); + card.appendChild(costBadge); + + // NAME (Middle/Top area roughly) + const nameEl = document.createElement('div'); + nameEl.textContent = spell.name.toUpperCase(); + Object.assign(nameEl.style, { + position: 'absolute', + top: '45px', // Adjusted for template header + width: '100%', + textAlign: 'center', + fontSize: '14px', + color: '#000', + fontWeight: 'bold', + fontFamily: '"Cinzel", serif', + padding: '0 10px', + boxSizing: 'border-box' + }); + card.appendChild(nameEl); + + // DESCRIPTION (Bottom area) + const descEl = document.createElement('div'); + descEl.textContent = spell.description; + Object.assign(descEl.style, { + position: 'absolute', + bottom: '30px', + left: '10px', + width: '160px', + height: '80px', + fontSize: '11px', + color: '#000', + textAlign: 'center', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontFamily: 'serif', + lineHeight: '1.2' + }); + card.appendChild(descEl); + + container.appendChild(card); + }); + + document.body.appendChild(container); + this.spellBookContainer = container; + + // Close on click outside (simple implementation) + const closeHandler = (e) => { + if (this.spellBookContainer && !this.spellBookContainer.contains(e.target) && e.target !== this.spellBookContainer) { + // We don't auto close here to avoid conflicts, just relying on toggle or card click + } + }; + } + + getSpellTemplate(type) { + // Return CSS url string based on type + // Templates: attack_template.png, defense_template.png, healing_template.png + let filename = 'attack_template.png'; + if (type === 'heal') filename = 'healing_template.png'; + if (type === 'defense') filename = 'defense_template.png'; + + return `url('/assets/images/dungeon1/spells/${filename}')`; + } }