From e45207807dbdc39b89002bf0ced4262f89655267 Mon Sep 17 00:00:00 2001 From: Marti Vich Date: Thu, 8 Jan 2026 23:53:39 +0100 Subject: [PATCH] Refactor: UIManager modularization and Devlog update --- DEVLOG.md | 48 +- src/view/UIManager.js | 1562 ++------------------------------ src/view/ui/FeedbackUI.js | 196 ++++ src/view/ui/HUDManager.js | 256 ++++++ src/view/ui/PlacementUI.js | 140 +++ src/view/ui/SpellbookUI.js | 110 +++ src/view/ui/TurnStatusUI.js | 172 ++++ src/view/ui/UnitCardManager.js | 382 ++++++++ 8 files changed, 1371 insertions(+), 1495 deletions(-) create mode 100644 src/view/ui/FeedbackUI.js create mode 100644 src/view/ui/HUDManager.js create mode 100644 src/view/ui/PlacementUI.js create mode 100644 src/view/ui/SpellbookUI.js create mode 100644 src/view/ui/TurnStatusUI.js create mode 100644 src/view/ui/UnitCardManager.js diff --git a/DEVLOG.md b/DEVLOG.md index 86b1c76..88492c3 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -1,5 +1,51 @@ # Devlog - Warhammer Quest (Versión Web 3D) +## Sesión 10: Refactorización Arquitectónica de UI +**Fecha:** 8 de Enero de 2026 + +### Objetivos +- Reducir la complejidad del `UIManager.js` (que superaba las 1500 líneas). +- Modularizar la interfaz para facilitar el mantenimiento y la escalabilidad. +- Separar responsabilidades claras entre HUD, Cartas de Unidad, Feedback, etc. + +### Cambios Realizados + +#### 1. Modularización de UIManager +Se ha dividido el monolito `UIManager.js` en 6 componentes especializados ubicados en `src/view/ui/`: + +* **`HUDManager.js`**: + * Gestiona elementos estáticos de pantalla (Minimapa, Controles de Cámara, Zoom). + * Mantiene el bucle de renderizado del minimapa 2D. +* **`UnitCardManager.js`**: + * Controla el panel lateral izquierdo con las fichas de Héroes y Monstruos. + * Maneja los botones de acción contextual (Atacar, Disparar, Inventario). +* **`TurnStatusUI.js`**: + * Panel superior central. Muestra Fase actual, Turno y botón de "Fin de Fase". + * Visualiza los resultados de la Fase de Poder. +* **`PlacementUI.js`**: + * Interfaz específica para la colocación de losetas (flechas de control, rotar, confirmar/cancelar). +* **`FeedbackUI.js`**: + * Sistema centralizado de comunicación con el usuario. + * Gestiona Modales, Ventanas de Confirmación y Mensajes Flotantes. + * Implementa el **Log de Combate** (anteriormente notificación simple). +* **`SpellbookUI.js`**: + * Módulo independiente para el libro de hechizos visual del Mago. + +#### 2. UIManager como Orquestador +El archivo principal `UIManager.js` se ha reducido drásticamente (~140 líneas). Ahora actúa únicamente como "pegamento": +- Inicializa los subsistemas. +- Escucha eventos del `GameEngine` (selección de entidades, cambio de fase). +- Delega la actualización de la interfaz a los módulos correspondientes. + +### Estado Actual +La refactorización es totalmente transparente para el usuario final (la funcionalidad visual se mantiene idéntica), pero el código es ahora robusto, mantenible y listo para crecer sin convertirse en código espagueti. + +### Próximos Pasos +- Implementar la Gestión de Inventario real. +- Pulir efectos visuales de hechizos y combate. + +--- + ## Sesión 9: Pulido de Combate, UI de Hechizos y Buffs **Fecha:** 8 de Enero de 2026 @@ -420,5 +466,3 @@ 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 - - diff --git a/src/view/UIManager.js b/src/view/UIManager.js index d8d65e4..8de6144 100644 --- a/src/view/UIManager.js +++ b/src/view/UIManager.js @@ -1,23 +1,55 @@ -import { DIRECTIONS } from '../engine/dungeon/Constants.js'; -import { SPELLS } from '../engine/data/Spells.js'; +import { HUDManager } from './ui/HUDManager.js'; +import { UnitCardManager } from './ui/UnitCardManager.js'; +import { TurnStatusUI } from './ui/TurnStatusUI.js'; +import { PlacementUI } from './ui/PlacementUI.js'; +import { FeedbackUI } from './ui/FeedbackUI.js'; +import { SpellbookUI } from './ui/SpellbookUI.js'; export class UIManager { constructor(cameraManager, gameEngine) { this.cameraManager = cameraManager; this.game = gameEngine; - this.dungeon = gameEngine.dungeon; - this.selectedHero = null; - this.createHUD(); - this.createHeroCardsPanel(); // NEW: Hero stat cards - this.createGameStatusPanel(); // New Panel - this.setupMinimapLoop(); - this.setupGameListeners(); // New Listeners + this.container = this.createMainContainer(); - // Hook into engine callbacks for UI updates + // Instantiate Subsystems + this.hud = new HUDManager(this.container, cameraManager, gameEngine); + this.turnUI = new TurnStatusUI(this.container, gameEngine); + this.feedback = new FeedbackUI(this.container, gameEngine); + this.spellbook = new SpellbookUI(gameEngine); + + // Circular deps / callbacks + const cardCallbacks = { + showModal: (t, m, c) => this.feedback.showModal(t, m, c), + toggleSpellBook: (h) => this.spellbook.toggle(h) + }; + this.cards = new UnitCardManager(this.container, gameEngine, cardCallbacks); + + const placementCallbacks = { + showModal: (t, m) => this.feedback.showModal(t, m), + showConfirm: (t, m, c) => this.feedback.showConfirm(t, m, c) + }; + this.placement = new PlacementUI(this.container, gameEngine, placementCallbacks); + + this.selectedHero = null; // State tracking for coordination + + this.setupGameListeners(); + } + + createMainContainer() { + const c = document.createElement('div'); + Object.assign(c.style, { + position: 'absolute', top: '0', left: '0', width: '100%', height: '100%', pointerEvents: 'none' + }); + document.body.appendChild(c); + return c; + } + + setupGameListeners() { + // Entity Selection const originalSelect = this.game.onEntitySelect; this.game.onEntitySelect = (id, isSelected) => { - // 1. Call Renderer (was in main.js) + // 1. Call Renderer if (this.cameraManager && this.cameraManager.renderer) { this.cameraManager.renderer.toggleEntitySelection(id, isSelected); } else if (window.RENDERER) { @@ -31,1510 +63,54 @@ export class UIManager { if (hero) { this.selectedHero = hero; - this.updateHeroStats(hero); - this.showHeroCard(hero); - this.hideMonsterCard(); // Hide monster card if showing - } else if (monster && this.selectedHero && this.game.turnManager.currentPhase === 'hero') { - // Show monster card only if a hero is selected (for attacking) - this.showMonsterCard(monster); + this.turnUI.updateHeroStats(hero); // Update top panel info + this.cards.showHeroCard(hero); + this.cards.hideMonsterCard(); + } else if (monster) { + // Check context: are we selecting a target? + if (this.selectedHero && this.game.turnManager.currentPhase === 'hero') { + this.cards.showMonsterCard(monster); + } } } else { - // Deselection - check what type was deselected + // Deselection if (this.selectedHero && this.selectedHero.id === id) { - // Hero was deselected this.selectedHero = null; - this.updateHeroStats(null); - this.hideHeroCard(); + this.turnUI.updateHeroStats(null); + this.cards.hideHeroCard(); } else { - // Monster was deselected - this.hideMonsterCard(); + this.cards.hideMonsterCard(); } } }; + // Entity Move const originalMove = this.game.onEntityMove; this.game.onEntityMove = (entity, path) => { if (originalMove) originalMove(entity, path); - this.updateHeroStats(entity); - // Update hero card if it's a hero + this.turnUI.updateHeroStats(entity); if (entity.type === 'hero') { - this.updateHeroCard(entity.id); - } - }; - } - - createHUD() { - // Container - this.container = document.createElement('div'); - this.container.style.position = 'absolute'; - this.container.style.top = '0'; - this.container.style.left = '0'; - this.container.style.width = '100%'; - this.container.style.height = '100%'; - this.container.style.pointerEvents = 'none'; // Click through to 3D scene - document.body.appendChild(this.container); - - // --- Minimap (Top Left) --- - this.minimapCanvas = document.createElement('canvas'); - this.minimapCanvas.width = 200; - this.minimapCanvas.height = 200; - this.minimapCanvas.style.position = 'absolute'; - this.minimapCanvas.style.top = '10px'; - this.minimapCanvas.style.left = '10px'; - this.minimapCanvas.style.border = '2px solid #444'; - this.minimapCanvas.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'; - this.minimapCanvas.style.pointerEvents = 'auto'; // Allow interaction if needed - this.container.appendChild(this.minimapCanvas); - - this.ctx = this.minimapCanvas.getContext('2d'); - - // --- Camera Controls (Top Right) --- - const controlsContainer = document.createElement('div'); - controlsContainer.style.position = 'absolute'; - controlsContainer.style.top = '20px'; - controlsContainer.style.right = '20px'; - controlsContainer.style.display = 'flex'; - controlsContainer.style.gap = '10px'; - controlsContainer.style.alignItems = 'center'; - controlsContainer.style.pointerEvents = 'auto'; - this.container.appendChild(controlsContainer); - - // Zoom slider (vertical) - const zoomContainer = document.createElement('div'); - zoomContainer.style.display = 'flex'; - zoomContainer.style.flexDirection = 'column'; - zoomContainer.style.alignItems = 'center'; - zoomContainer.style.gap = '0px'; - zoomContainer.style.height = '140px'; // Fixed height to accommodate label + slider - - // Zoom label - const zoomLabel = document.createElement('div'); - zoomLabel.textContent = 'Zoom'; - zoomLabel.style.color = '#fff'; - zoomLabel.style.fontSize = '15px'; - zoomLabel.style.fontFamily = 'sans-serif'; - zoomLabel.style.marginBottom = '10px'; - zoomLabel.style.marginTop = '0px'; - - const zoomSlider = document.createElement('input'); - zoomSlider.type = 'range'; - zoomSlider.min = '3'; - zoomSlider.max = '15'; - zoomSlider.value = '6'; - zoomSlider.step = '0.5'; - zoomSlider.style.width = '100px'; - zoomSlider.style.transform = 'rotate(-90deg)'; - zoomSlider.style.transformOrigin = 'center'; - zoomSlider.style.cursor = 'pointer'; - zoomSlider.style.marginTop = '40px'; - - this.zoomSlider = zoomSlider; - - // Set initial zoom - this.cameraManager.zoomLevel = 6; - this.cameraManager.updateProjection(); - - this.cameraManager.onZoomChange = (val) => { - if (this.zoomSlider) this.zoomSlider.value = val; - }; - - zoomSlider.oninput = (e) => { - this.cameraManager.zoomLevel = parseFloat(e.target.value); - this.cameraManager.updateProjection(); - }; - - zoomContainer.appendChild(zoomLabel); - zoomContainer.appendChild(zoomSlider); - - // 2D/3D Toggle Button - const toggleViewBtn = document.createElement('button'); - toggleViewBtn.textContent = '3D'; - toggleViewBtn.title = 'Cambiar vista 2D/3D'; - toggleViewBtn.style.width = '40px'; - toggleViewBtn.style.height = '40px'; - toggleViewBtn.style.borderRadius = '5px'; - toggleViewBtn.style.border = '1px solid #aaa'; // Slightly softer border - toggleViewBtn.style.backgroundColor = 'rgba(0, 0, 0, 0.6)'; - toggleViewBtn.style.color = '#daa520'; // Gold text - toggleViewBtn.style.cursor = 'pointer'; - toggleViewBtn.style.fontFamily = '"Cinzel", serif'; - toggleViewBtn.style.fontWeight = 'bold'; - toggleViewBtn.style.fontSize = '14px'; - toggleViewBtn.style.display = 'flex'; - toggleViewBtn.style.alignItems = 'center'; - toggleViewBtn.style.justifyContent = 'center'; - - toggleViewBtn.onmouseover = () => { toggleViewBtn.style.backgroundColor = 'rgba(0, 0, 0, 0.9)'; toggleViewBtn.style.color = '#fff'; }; - toggleViewBtn.onmouseout = () => { toggleViewBtn.style.backgroundColor = 'rgba(0, 0, 0, 0.6)'; toggleViewBtn.style.color = '#daa520'; }; - - toggleViewBtn.onclick = () => { - if (this.cameraManager) { - this.cameraManager.onAnimationComplete = null; - - // Determine if we are ABOUT to switch to 3D (currently 2D) - const isCurrently2D = (this.cameraManager.viewMode === '2D'); - - if (isCurrently2D) { - // Start of 2D -> 3D transition: Hide tokens immediately - if (this.cameraManager.renderer) { - this.cameraManager.renderer.hideTokens(); - } - } - - const is3D = this.cameraManager.toggleViewMode(); - toggleViewBtn.textContent = is3D ? '3D' : '2D'; - - // If we switched to 2D (is3D === false), show tokens AFTER animation - if (!is3D) { - this.cameraManager.onAnimationComplete = () => { - if (this.cameraManager.renderer) { - this.cameraManager.renderer.showTokens(this.game.heroes, this.game.monsters); - } - }; - } + this.cards.updateHeroCard(entity.id); } }; - // Direction buttons grid - const buttonsGrid = document.createElement('div'); - buttonsGrid.style.display = 'grid'; - buttonsGrid.style.gridTemplateColumns = '40px 40px 40px'; - buttonsGrid.style.gap = '5px'; - - controlsContainer.appendChild(toggleViewBtn); // Leftmost - controlsContainer.appendChild(zoomContainer); - controlsContainer.appendChild(buttonsGrid); - - const createBtn = (label, dir) => { - const btn = document.createElement('button'); - btn.textContent = label; - btn.style.width = '40px'; - btn.style.height = '40px'; - btn.style.backgroundColor = '#333'; - btn.style.color = '#fff'; - btn.style.border = '1px solid #666'; - btn.style.cursor = 'pointer'; - btn.style.transition = 'background-color 0.2s'; - btn.dataset.direction = dir; // Store direction for later reference - btn.onclick = () => { - this.cameraManager.setIsoView(dir); - this.updateActiveViewButton(dir); - }; - return btn; - }; - - // Layout: [N] - // [W] [E] - // [S] - - // Grid cells: 1 2 3 - const btnN = createBtn('N', DIRECTIONS.NORTH); btnN.style.gridColumn = '2'; - const btnW = createBtn('W', DIRECTIONS.WEST); btnW.style.gridColumn = '1'; - const btnE = createBtn('E', DIRECTIONS.EAST); btnE.style.gridColumn = '3'; - const btnS = createBtn('S', DIRECTIONS.SOUTH); btnS.style.gridColumn = '2'; - - buttonsGrid.appendChild(btnN); - buttonsGrid.appendChild(btnW); - buttonsGrid.appendChild(btnE); - buttonsGrid.appendChild(btnS); - - // Store button references for later updates - this.viewButtons = [btnN, btnE, btnS, btnW]; - - // Set initial active button (North) - this.updateActiveViewButton(DIRECTIONS.NORTH); - - // --- Tile Placement Controls (Bottom Center) --- - this.placementPanel = document.createElement('div'); - this.placementPanel.style.position = 'absolute'; - this.placementPanel.style.bottom = '20px'; - this.placementPanel.style.left = '50%'; - this.placementPanel.style.transform = 'translateX(-50%)'; - this.placementPanel.style.display = 'none'; // Hidden by default - this.placementPanel.style.pointerEvents = 'auto'; - this.placementPanel.style.backgroundColor = 'rgba(0, 0, 0, 0.85)'; - this.placementPanel.style.padding = '15px'; - this.placementPanel.style.borderRadius = '8px'; - this.placementPanel.style.border = '2px solid #666'; - this.container.appendChild(this.placementPanel); - - // Status text - this.placementStatus = document.createElement('div'); - this.placementStatus.style.color = '#fff'; - this.placementStatus.style.fontSize = '16px'; - this.placementStatus.style.fontFamily = 'sans-serif'; - this.placementStatus.style.marginBottom = '10px'; - this.placementStatus.style.textAlign = 'center'; - this.placementStatus.textContent = 'Coloca la loseta'; - this.placementPanel.appendChild(this.placementStatus); - - // Controls container - const placementControls = document.createElement('div'); - placementControls.style.display = 'flex'; - placementControls.style.gap = '15px'; - placementControls.style.alignItems = 'center'; - this.placementPanel.appendChild(placementControls); - - // Movement arrows (4-way grid) - const arrowGrid = document.createElement('div'); - arrowGrid.style.display = 'grid'; - arrowGrid.style.gridTemplateColumns = '40px 40px 40px'; - arrowGrid.style.gap = '3px'; - - const createArrow = (label, dx, dy) => { - const btn = document.createElement('button'); - btn.textContent = label; - btn.style.width = '40px'; - btn.style.height = '40px'; - btn.style.backgroundColor = '#444'; - btn.style.color = '#fff'; - btn.style.border = '1px solid #888'; - btn.style.cursor = 'pointer'; - btn.style.fontSize = '18px'; - btn.onclick = () => { - if (this.dungeon) { - this.dungeon.movePlacement(dx, dy); - } - }; - return btn; - }; - - const arrowUp = createArrow('↑', 0, 1); - const arrowLeft = createArrow('←', -1, 0); - const arrowRight = createArrow('→', 1, 0); - const arrowDown = createArrow('↓', 0, -1); - - arrowUp.style.gridColumn = '2'; - arrowLeft.style.gridColumn = '1'; - arrowRight.style.gridColumn = '3'; - arrowDown.style.gridColumn = '2'; - - arrowGrid.appendChild(arrowUp); - arrowGrid.appendChild(arrowLeft); - arrowGrid.appendChild(arrowRight); - arrowGrid.appendChild(arrowDown); - - placementControls.appendChild(arrowGrid); - - // Rotate button - this.rotateBtn = document.createElement('button'); - this.rotateBtn.textContent = '🔄 Rotar'; - this.rotateBtn.style.padding = '10px 20px'; - this.rotateBtn.style.backgroundColor = '#555'; - this.rotateBtn.style.color = '#fff'; - this.rotateBtn.style.border = '1px solid #888'; - this.rotateBtn.style.cursor = 'pointer'; - this.rotateBtn.style.fontSize = '16px'; - this.rotateBtn.style.borderRadius = '4px'; - this.rotateBtn.onclick = () => { - if (this.dungeon) { - this.dungeon.rotatePlacement(); - } - }; - placementControls.appendChild(this.rotateBtn); - - this.placeBtn = document.createElement('button'); - this.placeBtn.textContent = '⬇ Bajar'; - this.placeBtn.style.padding = '10px 20px'; - this.placeBtn.style.backgroundColor = '#2a5'; - this.placeBtn.style.color = '#fff'; - this.placeBtn.style.border = '1px solid #888'; - this.placeBtn.style.cursor = 'pointer'; - this.placeBtn.style.fontSize = '16px'; - this.placeBtn.style.borderRadius = '4px'; - this.placeBtn.onclick = () => { - if (this.dungeon) { - const success = this.dungeon.confirmPlacement(); - if (!success) { - this.showModal('Error de Colocación', 'No se puede colocar la loseta en esta posición.'); - } - } - }; - placementControls.appendChild(this.placeBtn); - - // Discard button - this.discardBtn = document.createElement('button'); - this.discardBtn.textContent = '❌ Cancelar'; - this.discardBtn.style.padding = '10px 20px'; - this.discardBtn.style.backgroundColor = '#d33'; - this.discardBtn.style.color = '#fff'; - this.discardBtn.style.border = '1px solid #888'; - this.discardBtn.style.cursor = 'pointer'; - this.discardBtn.style.fontSize = '16px'; - this.discardBtn.style.borderRadius = '4px'; - this.discardBtn.onclick = () => { - if (this.dungeon) { - this.showConfirm( - 'Confirmar acción', - '¿Quieres descartar esta loseta y bloquear la puerta?', - () => { - this.dungeon.cancelPlacement(); - } - ); - } - }; - placementControls.appendChild(this.discardBtn); - } - - createHeroCardsPanel() { - // Container for character cards (left side) - this.cardsContainer = document.createElement('div'); - this.cardsContainer.style.position = 'absolute'; - this.cardsContainer.style.left = '10px'; - this.cardsContainer.style.top = '220px'; // Below minimap - this.cardsContainer.style.display = 'flex'; - this.cardsContainer.style.flexDirection = 'column'; - this.cardsContainer.style.gap = '10px'; - this.cardsContainer.style.pointerEvents = 'auto'; - this.cardsContainer.style.width = '200px'; - this.container.appendChild(this.cardsContainer); - - // Create placeholder card - this.createPlaceholderCard(); - - // Store references - this.currentHeroCard = null; - this.currentMonsterCard = null; - this.attackButton = null; - } - - createPlaceholderCard() { - const card = document.createElement('div'); - card.style.width = '180px'; - card.style.height = '280px'; - card.style.backgroundColor = 'rgba(20, 20, 20, 0.95)'; - card.style.border = '2px solid #8B4513'; - card.style.borderRadius = '8px'; - card.style.padding = '10px'; - card.style.fontFamily = '"Cinzel", serif'; - card.style.color = '#888'; - card.style.display = 'flex'; - card.style.flexDirection = 'column'; - card.style.alignItems = 'center'; - card.style.justifyContent = 'center'; - card.style.textAlign = 'center'; - - // Circular icon container - const iconContainer = document.createElement('div'); - iconContainer.style.width = '100px'; - iconContainer.style.height = '100px'; - iconContainer.style.borderRadius = '50%'; - iconContainer.style.border = '2px solid #8B4513'; - iconContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; - iconContainer.style.display = 'flex'; - iconContainer.style.alignItems = 'center'; - iconContainer.style.justifyContent = 'center'; - iconContainer.style.marginBottom = '20px'; - - const icon = document.createElement('div'); - icon.textContent = '🎴'; - icon.style.fontSize = '48px'; - iconContainer.appendChild(icon); - card.appendChild(iconContainer); - - const text = document.createElement('div'); - text.textContent = 'Selecciona un Aventurero'; - text.style.fontSize = '14px'; - text.style.color = '#DAA520'; - card.appendChild(text); - - this.placeholderCard = card; - this.cardsContainer.appendChild(card); - } - - createHeroCard(hero) { - const card = document.createElement('div'); - card.style.width = '180px'; - card.style.backgroundColor = 'rgba(20, 20, 20, 0.95)'; - card.style.border = '2px solid #8B4513'; - card.style.borderRadius = '8px'; - card.style.padding = '10px'; - card.style.fontFamily = '"Cinzel", serif'; - card.style.color = '#fff'; - card.style.transition = 'all 0.3s'; - card.style.cursor = 'pointer'; - - // Hover effect - card.onmouseenter = () => { - card.style.borderColor = '#DAA520'; - card.style.transform = 'scale(1.05)'; - }; - card.onmouseleave = () => { - card.style.borderColor = '#8B4513'; - card.style.transform = 'scale(1)'; - }; - - // Click to select hero - card.onclick = () => { - if (this.game.onCellClick) { - this.game.onCellClick(hero.x, hero.y); - } - }; - - // Portrait (circular) - const portrait = document.createElement('div'); - portrait.style.width = '100px'; - portrait.style.height = '100px'; - portrait.style.borderRadius = '50%'; - portrait.style.overflow = 'hidden'; - portrait.style.border = '2px solid #DAA520'; - portrait.style.marginBottom = '8px'; - portrait.style.marginLeft = 'auto'; - portrait.style.marginRight = 'auto'; - portrait.style.backgroundColor = '#000'; - portrait.style.display = 'flex'; - portrait.style.alignItems = 'center'; - portrait.style.justifyContent = 'center'; - - // Use token image (placeholder for now) - const tokenPath = `/assets/images/dungeon1/tokens/heroes/${hero.key}.png?v=2`; - const img = document.createElement('img'); - img.src = tokenPath; - img.style.width = '100%'; - img.style.height = '100%'; - img.style.objectFit = 'cover'; - - // Fallback if image doesn't exist - img.onerror = () => { - portrait.innerHTML = `
?
`; - }; - - portrait.appendChild(img); - card.appendChild(portrait); - - // Name - const name = document.createElement('div'); - name.textContent = hero.name; - name.style.fontSize = '16px'; - name.style.fontWeight = 'bold'; - name.style.color = '#DAA520'; - name.style.textAlign = 'center'; - name.style.marginBottom = '8px'; - name.style.textTransform = 'uppercase'; - card.appendChild(name); - - // Lantern indicator - if (hero.hasLantern) { - const lantern = document.createElement('div'); - lantern.textContent = '🏮 Portador de la Lámpara'; - lantern.style.fontSize = '10px'; - lantern.style.color = '#FFA500'; - lantern.style.textAlign = 'center'; - lantern.style.marginBottom = '8px'; - card.appendChild(lantern); - } - - // Stats grid - const statsGrid = document.createElement('div'); - statsGrid.style.display = 'grid'; - statsGrid.style.gridTemplateColumns = '1fr 1fr'; - statsGrid.style.gap = '4px'; - statsGrid.style.fontSize = '12px'; - statsGrid.style.marginBottom = '8px'; - - const stats = [ - { label: 'H.C', value: hero.stats.ws || 0 }, // Hab. Combate - { label: 'H.P', value: hero.stats.bs || 0 }, // Hab. Proyectiles - { label: 'Fuer', value: hero.stats.str || 0 }, // Fuerza - { label: 'Res', value: hero.stats.toughness || 0 }, // Resistencia - { label: 'Her', value: `${hero.currentWounds || hero.stats.wounds}/${hero.stats.wounds}` }, // Heridas - { label: 'Ini', value: hero.stats.initiative || 0 },// Iniciativa - { label: 'Ata', value: hero.stats.attacks || 0 }, // Ataques - { label: 'Mov', value: `${hero.currentMoves || 0}/${hero.stats.move}` } // Movimiento - ]; - - stats.forEach(stat => { - const statEl = document.createElement('div'); - statEl.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; - statEl.style.padding = '3px 5px'; - statEl.style.borderRadius = '3px'; - statEl.style.display = 'flex'; - statEl.style.justifyContent = 'space-between'; - - const label = document.createElement('span'); - label.textContent = stat.label + ':'; - label.style.color = '#AAA'; - - const value = document.createElement('span'); - value.textContent = stat.value; - value.style.color = '#FFF'; - value.style.fontWeight = 'bold'; - - statEl.appendChild(label); - statEl.appendChild(value); - statsGrid.appendChild(statEl); - }); - - card.appendChild(statsGrid); - - // Ranged Attack Button (Elf Only) - if (hero.key === 'elf') { - const isPinned = this.game.isEntityPinned(hero); - const hasAttacked = hero.hasAttacked; - - const bowBtn = document.createElement('button'); - bowBtn.textContent = hasAttacked ? '🏹 YA DISPARADO' : '🏹 DISPARAR ARCO'; - bowBtn.style.width = '100%'; - bowBtn.style.padding = '8px'; - bowBtn.style.marginTop = '8px'; - - const isDisabled = isPinned || hasAttacked; - - bowBtn.style.backgroundColor = isDisabled ? '#555' : '#2E8B57'; // SeaGreen - bowBtn.style.color = '#fff'; - bowBtn.style.border = '1px solid #fff'; - bowBtn.style.borderRadius = '4px'; - bowBtn.style.fontFamily = '"Cinzel", serif'; - bowBtn.style.cursor = isDisabled ? 'not-allowed' : 'pointer'; - - if (isPinned) { - bowBtn.title = "¡Estás trabado en combate cuerpo a cuerpo!"; - } else if (hasAttacked) { - bowBtn.title = "Ya has atacado en esta fase."; - } else { - bowBtn.onclick = (e) => { - e.stopPropagation(); // Prevent card click propagation if any - this.game.startRangedTargeting(); - // Provide immediate feedback? - this.showModal('Modo Disparo', 'Selecciona un enemigo visible para disparar.'); - }; - } - card.appendChild(bowBtn); - } - - // 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); - - // 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'; - - spellsBtn.onclick = (e) => { - e.stopPropagation(); - // Toggle Spell Book UI - this.toggleSpellBook(hero); - }; - - card.appendChild(spellsBtn); - } - - card.dataset.heroId = hero.id; - - return card; - } - - showHeroCard(hero) { - // Remove placeholder if present - if (this.placeholderCard && this.placeholderCard.parentNode) { - this.cardsContainer.removeChild(this.placeholderCard); - } - - // Remove previous hero card if present - if (this.currentHeroCard && this.currentHeroCard.parentNode) { - this.cardsContainer.removeChild(this.currentHeroCard); - } - - // Create and show new hero card - this.currentHeroCard = this.createHeroCard(hero); - this.cardsContainer.insertBefore(this.currentHeroCard, this.cardsContainer.firstChild); - } - - hideHeroCard() { - // Remove hero card - if (this.currentHeroCard && this.currentHeroCard.parentNode) { - this.cardsContainer.removeChild(this.currentHeroCard); - this.currentHeroCard = null; - } - - // Show placeholder if no cards are visible - if (!this.currentMonsterCard && this.placeholderCard && !this.placeholderCard.parentNode) { - this.cardsContainer.appendChild(this.placeholderCard); - } - } - - updateHeroCard(heroId) { - if (!this.currentHeroCard || this.currentHeroCard.dataset.heroId !== heroId) { - return; - } - - const hero = this.game.heroes.find(h => h.id === heroId); - if (!hero) return; - - // Update wounds and moves in the stats grid - const statsGrid = this.currentHeroCard.querySelector('div[style*="grid-template-columns"]'); - if (statsGrid) { - const statDivs = statsGrid.children; - // W is at index 4, Mov is at index 7 - if (statDivs[4]) { - const wValue = statDivs[4].querySelector('span:last-child'); - if (wValue) wValue.textContent = `${hero.currentWounds || hero.stats.wounds}/${hero.stats.wounds}`; - } - if (statDivs[7]) { - const movValue = statDivs[7].querySelector('span:last-child'); - if (movValue) movValue.textContent = `${hero.currentMoves || 0}/${hero.stats.move}`; - } - } - } - - createMonsterCard(monster) { - const card = document.createElement('div'); - card.style.width = '180px'; - card.style.backgroundColor = 'rgba(40, 20, 20, 0.95)'; - card.style.border = '2px solid #8B0000'; - card.style.borderRadius = '8px'; - card.style.padding = '10px'; - card.style.fontFamily = '"Cinzel", serif'; - card.style.color = '#fff'; - - const portrait = document.createElement('div'); - portrait.style.width = '100px'; - portrait.style.height = '100px'; - portrait.style.borderRadius = '50%'; - portrait.style.overflow = 'hidden'; - portrait.style.border = '2px solid #8B0000'; - portrait.style.marginBottom = '8px'; - portrait.style.marginLeft = 'auto'; - portrait.style.marginRight = 'auto'; - portrait.style.backgroundColor = '#000'; - portrait.style.display = 'flex'; - portrait.style.alignItems = 'center'; - portrait.style.justifyContent = 'center'; - - const tokenPath = `/assets/images/dungeon1/tokens/enemies/${monster.key}.png?v=2`; - const img = document.createElement('img'); - img.src = tokenPath; - img.style.width = '100%'; - img.style.height = '100%'; - img.style.objectFit = 'cover'; - - img.onerror = () => { - portrait.innerHTML = `
👹
`; - }; - - portrait.appendChild(img); - card.appendChild(portrait); - - const name = document.createElement('div'); - name.textContent = monster.name; - name.style.fontSize = '16px'; - name.style.fontWeight = 'bold'; - name.style.color = '#FF4444'; - name.style.textAlign = 'center'; - name.style.marginBottom = '8px'; - name.style.textTransform = 'uppercase'; - card.appendChild(name); - - const statsGrid = document.createElement('div'); - statsGrid.style.display = 'grid'; - statsGrid.style.gridTemplateColumns = '1fr 1fr'; - statsGrid.style.gap = '4px'; - statsGrid.style.fontSize = '12px'; - - const stats = [ - { label: 'H.C', value: monster.stats.ws || 0 }, // Hab. Combate - { label: 'Fuer', value: monster.stats.str || 0 }, // Fuerza - { label: 'Res', value: monster.stats.toughness || 0 }, // Resistencia - { label: 'Her', value: `${monster.currentWounds || monster.stats.wounds}/${monster.stats.wounds}` }, // Heridas - { label: 'Ini', value: monster.stats.initiative || 0 }, // Iniciativa - { label: 'Ata', value: monster.stats.attacks || 0 } // Ataques - ]; - - stats.forEach(stat => { - const statEl = document.createElement('div'); - statEl.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; - statEl.style.padding = '3px 5px'; - statEl.style.borderRadius = '3px'; - statEl.style.display = 'flex'; - statEl.style.justifyContent = 'space-between'; - - const label = document.createElement('span'); - label.textContent = stat.label + ':'; - label.style.color = '#AAA'; - - const value = document.createElement('span'); - value.textContent = stat.value; - value.style.color = '#FFF'; - value.style.fontWeight = 'bold'; - - statEl.appendChild(label); - statEl.appendChild(value); - statsGrid.appendChild(statEl); - }); - - card.appendChild(statsGrid); - card.dataset.monsterId = monster.id; - - return card; - } - - showMonsterCard(monster) { - if (this.currentMonsterCard && this.currentMonsterCard.parentNode) { - this.cardsContainer.removeChild(this.currentMonsterCard); - } - - if (this.attackButton && this.attackButton.parentNode) { - this.cardsContainer.removeChild(this.attackButton); - } - - this.currentMonsterCard = this.createMonsterCard(monster); - this.cardsContainer.appendChild(this.currentMonsterCard); - - this.attackButton = document.createElement('button'); - this.attackButton.textContent = '⚔️ ATACAR'; - this.attackButton.style.width = '180px'; - this.attackButton.style.padding = '12px'; - this.attackButton.style.backgroundColor = '#8B0000'; - this.attackButton.style.color = '#fff'; - this.attackButton.style.border = '2px solid #FF4444'; - this.attackButton.style.borderRadius = '8px'; - this.attackButton.style.fontFamily = '"Cinzel", serif'; - this.attackButton.style.fontSize = '16px'; - this.attackButton.style.fontWeight = 'bold'; - this.attackButton.style.cursor = 'pointer'; - this.attackButton.style.transition = 'all 0.2s'; - - this.attackButton.onmouseenter = () => { - this.attackButton.style.backgroundColor = '#FF0000'; - this.attackButton.style.transform = 'scale(1.05)'; - }; - this.attackButton.onmouseleave = () => { - this.attackButton.style.backgroundColor = '#8B0000'; - this.attackButton.style.transform = 'scale(1)'; - }; - - this.attackButton.onclick = () => { - if (this.game.performHeroAttack) { - const result = this.game.performHeroAttack(monster.id); - if (result && result.success) { - // Attack successful, hide monster card - this.hideMonsterCard(); - // Deselect monster - if (this.game.selectedMonster) { - if (this.game.onEntitySelect) { - this.game.onEntitySelect(this.game.selectedMonster.id, false); - } - this.game.selectedMonster = null; - } - } - } - }; - - this.cardsContainer.appendChild(this.attackButton); - } - - showRangedAttackUI(monster) { - this.showMonsterCard(monster); - - if (this.attackButton) { - this.attackButton.textContent = '🏹 DISPARAR'; - this.attackButton.style.backgroundColor = '#2E8B57'; - this.attackButton.style.border = '2px solid #32CD32'; - - this.attackButton.onclick = () => { - const result = this.game.performRangedAttack(monster.id); - if (result && result.success) { - this.game.cancelTargeting(); - this.hideMonsterCard(); // Hide UI - // Also clear renderer - this.game.deselectEntity(); // Deselect hero too? "desparecerá todo". - // Let's interpret "desaparecerá todo" as targeting visuals and Shoot button. - // But usually in game we keep hero selected. - // If we deselect everything: - // this.game.deselectEntity(); - // Let's keep hero selected for flow, but clear targeting. - } - }; - - this.attackButton.onmouseenter = () => { - this.attackButton.style.backgroundColor = '#3CB371'; - this.attackButton.style.transform = 'scale(1.05)'; - }; - this.attackButton.onmouseleave = () => { - this.attackButton.style.backgroundColor = '#2E8B57'; - this.attackButton.style.transform = 'scale(1)'; - }; - } - } - - hideMonsterCard() { - if (this.currentMonsterCard && this.currentMonsterCard.parentNode) { - this.cardsContainer.removeChild(this.currentMonsterCard); - this.currentMonsterCard = null; - } - - if (this.attackButton && this.attackButton.parentNode) { - this.cardsContainer.removeChild(this.attackButton); - this.attackButton = null; - } - } - - showPlacementControls(show) { - if (this.placementPanel) { - this.placementPanel.style.display = show ? 'block' : 'none'; - } - } - - updatePlacementStatus(isValid) { - if (this.placementStatus) { - if (isValid) { - this.placementStatus.textContent = '✅ Posición válida'; - this.placementStatus.style.color = '#0f0'; - this.placeBtn.style.backgroundColor = '#2a5'; - this.placeBtn.style.cursor = 'pointer'; - } else { - this.placementStatus.textContent = '❌ Posición inválida'; - this.placementStatus.style.color = '#f44'; - this.placeBtn.style.backgroundColor = '#555'; - this.placeBtn.style.cursor = 'not-allowed'; - } - } - } - - updateActiveViewButton(activeDirection) { - // Reset all buttons to default color - this.viewButtons.forEach(btn => { - btn.style.backgroundColor = '#333'; - }); - - // Highlight the active button - const activeBtn = this.viewButtons.find(btn => btn.dataset.direction === activeDirection); - if (activeBtn) { - activeBtn.style.backgroundColor = '#f0c040'; // Yellow/gold color - } - } - - setupMinimapLoop() { - const loop = () => { - this.drawMinimap(); - requestAnimationFrame(loop); - }; - loop(); - } - - drawMinimap() { - const ctx = this.ctx; - const w = this.minimapCanvas.width; - const h = this.minimapCanvas.height; - - ctx.clearRect(0, 0, w, h); - - const cellSize = 5; - const centerX = w / 2; - const centerY = h / 2; - - ctx.fillStyle = '#666'; // Generic floor - - for (const [key, tileId] of this.dungeon.grid.occupiedCells) { - const [x, y] = key.split(',').map(Number); - const cx = centerX + (x * cellSize); - const cy = centerY - (y * cellSize); - - if (tileId.includes('room')) ctx.fillStyle = '#55a'; - else ctx.fillStyle = '#aaa'; - - ctx.fillRect(cx, cy, cellSize, cellSize); - } - - // Draw Exits (Available) - ctx.fillStyle = '#0f0'; // Green dots for open exits - if (this.dungeon.availableExits) { - this.dungeon.availableExits.forEach(exit => { - const ex = centerX + (exit.x * cellSize); - const ey = centerY - (exit.y * cellSize); - ctx.fillRect(ex, ey, cellSize, cellSize); - }); - } - - // Draw Entry (0,0) cross - ctx.strokeStyle = '#f00'; - ctx.beginPath(); - ctx.moveTo(centerX - 5, centerY); - ctx.lineTo(centerX + 5, centerY); - ctx.moveTo(centerX, centerY - 5); - ctx.lineTo(centerX, centerY + 5); - ctx.stroke(); - } - - showModal(title, message, onClose) { - // Overlay - const overlay = document.createElement('div'); - overlay.style.position = 'absolute'; - overlay.style.top = '0'; - overlay.style.left = '0'; - overlay.style.width = '100%'; - overlay.style.height = '100%'; - overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; - overlay.style.display = 'flex'; - overlay.style.justifyContent = 'center'; - overlay.style.alignItems = 'center'; - overlay.style.pointerEvents = 'auto'; // Block clicks behind - overlay.style.zIndex = '1000'; - - // Content Box - const content = document.createElement('div'); - content.style.backgroundColor = '#222'; - content.style.border = '2px solid #888'; - content.style.borderRadius = '8px'; - content.style.padding = '20px'; - content.style.width = '300px'; - content.style.textAlign = 'center'; - content.style.color = '#fff'; - content.style.fontFamily = 'sans-serif'; - - // Title - const titleEl = document.createElement('h2'); - titleEl.textContent = title; - titleEl.style.marginTop = '0'; - titleEl.style.color = '#f44'; // Reddish for importance - content.appendChild(titleEl); - - // Message - const msgEl = document.createElement('p'); - msgEl.innerHTML = message; - msgEl.style.fontSize = '16px'; - msgEl.style.lineHeight = '1.5'; - content.appendChild(msgEl); - - // OK Button - const btn = document.createElement('button'); - btn.textContent = 'Entendido'; - btn.style.marginTop = '20px'; - btn.style.padding = '10px 20px'; - btn.style.fontSize = '16px'; - btn.style.cursor = 'pointer'; - btn.style.backgroundColor = '#444'; - btn.style.color = '#fff'; - btn.style.border = '1px solid #888'; - btn.onclick = () => { - this.container.removeChild(overlay); - if (onClose) onClose(); - }; - content.appendChild(btn); - - overlay.appendChild(content); - this.container.appendChild(overlay); - } - - showCombatLog(log) { - if (!this.notificationArea) return; - - const isHit = log.hitSuccess; - const color = isHit ? '#ff4444' : '#aaaaaa'; - const title = isHit ? 'GOLPE!' : 'FALLO'; - - let detailHtml = ''; - if (isHit) { - if (log.woundsCaused > 0) { - detailHtml = `
-${log.woundsCaused} HP
`; - } else { - detailHtml = `
Sin Heridas (Armadura)
`; - } - } else { - detailHtml = `
Esquivado / Fallado
`; - } - - // Show simplified but impactful message - this.notificationArea.innerHTML = ` -
-
${log.attackerId.split('_')[0]} ATACA
- ${detailHtml} -
${log.message}
-
- `; - - this.notificationArea.style.opacity = '1'; - - // Update hero card if defender is a hero - const defender = this.game.heroes.find(h => h.id === log.defenderId) || - this.game.monsters.find(m => m.id === log.defenderId); - if (defender && defender.type === 'hero') { - this.updateHeroCard(defender.id); - } - - setTimeout(() => { - if (this.notificationArea) this.notificationArea.style.opacity = '0'; - }, 3500); - } - - showConfirm(title, message, onConfirm) { - // Overlay - const overlay = document.createElement('div'); - overlay.style.position = 'absolute'; - overlay.style.top = '0'; - overlay.style.left = '0'; - overlay.style.width = '100%'; - overlay.style.height = '100%'; - overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; - overlay.style.display = 'flex'; - overlay.style.justifyContent = 'center'; - overlay.style.alignItems = 'center'; - overlay.style.pointerEvents = 'auto'; // Block clicks behind - overlay.style.zIndex = '1000'; - - // Content Box - const content = document.createElement('div'); - content.style.backgroundColor = '#222'; - content.style.border = '2px solid #888'; - content.style.borderRadius = '8px'; - content.style.padding = '20px'; - content.style.width = '300px'; - content.style.textAlign = 'center'; - content.style.color = '#fff'; - content.style.fontFamily = 'sans-serif'; - - // Title - const titleEl = document.createElement('h2'); - titleEl.textContent = title; - titleEl.style.marginTop = '0'; - titleEl.style.color = '#f44'; - content.appendChild(titleEl); - - // Message - const msgEl = document.createElement('p'); - msgEl.innerHTML = message; - msgEl.style.fontSize = '16px'; - msgEl.style.lineHeight = '1.5'; - content.appendChild(msgEl); - - // Buttons Container - const buttons = document.createElement('div'); - buttons.style.display = 'flex'; - buttons.style.justifyContent = 'space-around'; - buttons.style.marginTop = '20px'; - - // Cancel Button - const cancelBtn = document.createElement('button'); - cancelBtn.textContent = 'Cancelar'; - cancelBtn.style.padding = '10px 20px'; - cancelBtn.style.fontSize = '16px'; - cancelBtn.style.cursor = 'pointer'; - cancelBtn.style.backgroundColor = '#555'; - cancelBtn.style.color = '#fff'; - cancelBtn.style.border = '1px solid #888'; - cancelBtn.onclick = () => { - this.container.removeChild(overlay); - }; - buttons.appendChild(cancelBtn); - - // Confirm Button - const confirmBtn = document.createElement('button'); - confirmBtn.textContent = 'Aceptar'; - confirmBtn.style.padding = '10px 20px'; - confirmBtn.style.fontSize = '16px'; - confirmBtn.style.cursor = 'pointer'; - confirmBtn.style.backgroundColor = '#2a5'; - confirmBtn.style.color = '#fff'; - confirmBtn.style.border = '1px solid #888'; - confirmBtn.onclick = () => { - if (onConfirm) onConfirm(); - this.container.removeChild(overlay); - }; - buttons.appendChild(confirmBtn); - - content.appendChild(buttons); - overlay.appendChild(content); - this.container.appendChild(overlay); - } - - createGameStatusPanel() { - // Top Center Panel - this.statusPanel = document.createElement('div'); - this.statusPanel.style.position = 'absolute'; - this.statusPanel.style.top = '20px'; - this.statusPanel.style.left = '50%'; - this.statusPanel.style.transform = 'translateX(-50%)'; - this.statusPanel.style.display = 'flex'; - this.statusPanel.style.flexDirection = 'column'; - this.statusPanel.style.alignItems = 'center'; - this.statusPanel.style.pointerEvents = 'none'; - - // Turn/Phase Info - this.phaseInfo = document.createElement('div'); - this.phaseInfo.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'; - this.phaseInfo.style.padding = '10px 20px'; - this.phaseInfo.style.border = '2px solid #daa520'; // GoldenRod - this.phaseInfo.style.borderRadius = '5px'; - this.phaseInfo.style.color = '#fff'; - this.phaseInfo.style.fontFamily = '"Cinzel", serif'; - this.phaseInfo.style.fontSize = '20px'; - this.phaseInfo.style.textAlign = 'center'; - this.phaseInfo.style.textTransform = 'uppercase'; - this.phaseInfo.style.width = '300px'; // Match button width - this.phaseInfo.innerHTML = ` -
Turn 1
-
Setup
- `; - - this.statusPanel.appendChild(this.phaseInfo); - - // End Phase Button - this.endPhaseBtn = document.createElement('button'); - this.endPhaseBtn.textContent = 'ACABAR FASE AVENTUREROS'; - this.endPhaseBtn.style.marginTop = '10px'; - this.endPhaseBtn.style.width = '300px'; // Fixed width to prevent resizing with messages - this.endPhaseBtn.style.padding = '8px'; - this.endPhaseBtn.style.backgroundColor = '#daa520'; // Gold - this.endPhaseBtn.style.color = '#000'; - this.endPhaseBtn.style.border = '1px solid #8B4513'; - this.endPhaseBtn.style.borderRadius = '3px'; - this.endPhaseBtn.style.fontWeight = 'bold'; - this.endPhaseBtn.style.cursor = 'pointer'; - this.endPhaseBtn.style.display = 'none'; // Hidden by default - this.endPhaseBtn.style.fontFamily = '"Cinzel", serif'; - this.endPhaseBtn.style.fontSize = '12px'; - this.endPhaseBtn.style.pointerEvents = 'auto'; // Enable clicking - - this.endPhaseBtn.onmouseover = () => { this.endPhaseBtn.style.backgroundColor = '#ffd700'; }; - this.endPhaseBtn.onmouseout = () => { this.endPhaseBtn.style.backgroundColor = '#daa520'; }; - - this.endPhaseBtn.onclick = () => { - console.log('[UIManager] End Phase Button Clicked', this.game.turnManager.currentPhase); - this.game.turnManager.nextPhase(); - }; - this.statusPanel.appendChild(this.endPhaseBtn); - - // Notification Area (Power Roll results, etc) - this.notificationArea = document.createElement('div'); - this.notificationArea.style.marginTop = '10px'; - this.notificationArea.style.maxWidth = '600px'; // Prevent very wide messages - this.notificationArea.style.transition = 'opacity 0.5s'; - this.notificationArea.style.opacity = '0'; - this.statusPanel.appendChild(this.notificationArea); - - this.container.appendChild(this.statusPanel); - - // Inject Font - if (!document.getElementById('game-font')) { - const link = document.createElement('link'); - link.id = 'game-font'; - link.href = 'https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&display=swap'; - link.rel = 'stylesheet'; - document.head.appendChild(link); - } - } - - setupGameListeners() { + // Turn Manager Events if (this.game.turnManager) { this.game.turnManager.on('phase_changed', (phase) => { - this.updatePhaseDisplay(phase); + this.turnUI.updatePhaseDisplay(phase, this.selectedHero); }); - this.game.turnManager.on('POWER_RESULT', (data) => { - this.showPowerRollResult(data); + this.turnUI.showPowerRollResult(data); }); } } - updatePhaseDisplay(phase) { - if (!this.phaseInfo) return; - const turn = this.game.turnManager.currentTurn; - - let content = ` -
Turn ${turn}
-
${phase.replace('_', ' ')}
- `; - - if (this.selectedHero) { - content += this.getHeroStatsHTML(this.selectedHero); - } - - this.phaseInfo.innerHTML = content; - - if (this.endPhaseBtn) { - if (phase === 'hero') { - this.endPhaseBtn.style.display = 'block'; - this.endPhaseBtn.textContent = 'ACABAR FASE AVENTUREROS'; - this.endPhaseBtn.title = "Pasar a la Fase de Monstruos"; - } else if (phase === 'exploration') { - this.endPhaseBtn.style.display = 'block'; - this.endPhaseBtn.textContent = 'ACABAR TURNO'; - this.endPhaseBtn.title = "Finalizar turno y comenzar Fase de Poder"; - } else { - this.endPhaseBtn.style.display = 'none'; - } - } - } - - updateHeroStats(hero) { - if (!this.phaseInfo) return; - - const turn = this.game.turnManager.currentTurn; - const phase = this.game.turnManager.currentPhase; - - let content = ` -
Turn ${turn}
-
${phase.replace('_', ' ')}
- `; - - if (hero) { - content += this.getHeroStatsHTML(hero); - } - - this.phaseInfo.innerHTML = content; - } - - getHeroStatsHTML(hero) { - const portraitUrl = hero.texturePath || ''; - - const lanternIcon = hero.hasLantern ? '🏮' : ''; - - return ` -
-
- ${hero.name} -
-
-
- ${hero.name} ${lanternIcon} -
-
- Moves: ${hero.currentMoves} / ${hero.stats.move} -
-
-
- `; - } - - showPowerRollResult(data) { - if (!this.notificationArea) return; - const { roll, message, eventTriggered } = data; - const color = eventTriggered ? '#ff4444' : '#44ff44'; - - this.notificationArea.innerHTML = ` -
-
Power Phase
-
${roll}
-
${message}
-
- `; - - this.notificationArea.style.opacity = '1'; - - setTimeout(() => { - if (this.notificationArea) this.notificationArea.style.opacity = '0'; - }, 3000); - } - showTemporaryMessage(title, message, duration = 2000) { - const modal = document.createElement('div'); - Object.assign(modal.style, { - position: 'absolute', - top: '25%', - left: '50%', - transform: 'translate(-50%, -50%)', - backgroundColor: 'rgba(139, 0, 0, 0.9)', - color: '#fff', - padding: '15px 30px', - borderRadius: '8px', - border: '2px solid #ff4444', - fontFamily: '"Cinzel", serif', - fontSize: '20px', - textShadow: '2px 2px 4px black', - zIndex: '2000', - pointerEvents: 'none', - opacity: '0', - transition: 'opacity 0.5s ease-in-out' - }); - - modal.innerHTML = ` -

⚠️ ${title}

-
${message}
- `; - - document.body.appendChild(modal); - - // Fade In - requestAnimationFrame(() => { modal.style.opacity = '1'; }); - - // Fade Out and Remove - setTimeout(() => { - modal.style.opacity = '0'; - setTimeout(() => { - if (modal.parentNode) document.body.removeChild(modal); - }, 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}')`; - } + // Public API for GameEngine access + showPlacementControls(show) { this.placement.showControls(show); } + updatePlacementStatus(valid) { this.placement.updateStatus(valid); } + 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); } + showRangedAttackUI(monster) { this.cards.showRangedAttackUI(monster); } } diff --git a/src/view/ui/FeedbackUI.js b/src/view/ui/FeedbackUI.js new file mode 100644 index 0000000..a88ea38 --- /dev/null +++ b/src/view/ui/FeedbackUI.js @@ -0,0 +1,196 @@ +export class FeedbackUI { + constructor(parentContainer, game) { + this.parentContainer = parentContainer; + this.game = game; // Needed for resolving hero names/ids in logs? + + this.combatLogContainer = null; + this.initCombatLogContainer(); + } + + initCombatLogContainer() { + this.combatLogContainer = document.createElement('div'); + Object.assign(this.combatLogContainer.style, { + position: 'absolute', + top: '140px', // Below the top status panel + left: '50%', + transform: 'translateX(-50%)', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + pointerEvents: 'none', + width: '100%', + maxWidth: '600px', + zIndex: '500' // Below modals + }); + this.parentContainer.appendChild(this.combatLogContainer); + } + + showModal(title, message, onClose) { + const overlay = document.createElement('div'); + Object.assign(overlay.style, { + position: 'absolute', top: '0', left: '0', width: '100%', height: '100%', + backgroundColor: 'rgba(0, 0, 0, 0.7)', display: 'flex', justifyContent: 'center', alignItems: 'center', + pointerEvents: 'auto', zIndex: '1000' + }); + + const content = document.createElement('div'); + Object.assign(content.style, { + backgroundColor: '#222', border: '2px solid #888', borderRadius: '8px', padding: '20px', + width: '300px', textAlign: 'center', color: '#fff', fontFamily: 'sans-serif' + }); + + const titleEl = document.createElement('h2'); + titleEl.textContent = title; + Object.assign(titleEl.style, { marginTop: '0', color: '#f44' }); + content.appendChild(titleEl); + + const msgEl = document.createElement('p'); + msgEl.innerHTML = message; + Object.assign(msgEl.style, { fontSize: '16px', lineHeight: '1.5' }); + content.appendChild(msgEl); + + const btn = document.createElement('button'); + btn.textContent = 'Entendido'; + Object.assign(btn.style, { + marginTop: '20px', padding: '10px 20px', fontSize: '16px', cursor: 'pointer', + backgroundColor: '#444', color: '#fff', border: '1px solid #888' + }); + btn.onclick = () => { + if (overlay.parentNode /** Checks if attached */) this.parentContainer.removeChild(overlay); + if (onClose) onClose(); + }; + content.appendChild(btn); + + overlay.appendChild(content); + this.parentContainer.appendChild(overlay); + } + + showConfirm(title, message, onConfirm) { + const overlay = document.createElement('div'); + Object.assign(overlay.style, { + position: 'absolute', top: '0', left: '0', width: '100%', height: '100%', + backgroundColor: 'rgba(0, 0, 0, 0.7)', display: 'flex', justifyContent: 'center', alignItems: 'center', + pointerEvents: 'auto', zIndex: '1000' + }); + + const content = document.createElement('div'); + Object.assign(content.style, { + backgroundColor: '#222', border: '2px solid #888', borderRadius: '8px', padding: '20px', + width: '300px', textAlign: 'center', color: '#fff', fontFamily: 'sans-serif' + }); + + const titleEl = document.createElement('h2'); + titleEl.textContent = title; + Object.assign(titleEl.style, { marginTop: '0', color: '#f44' }); + content.appendChild(titleEl); + + const msgEl = document.createElement('p'); + msgEl.innerHTML = message; + Object.assign(msgEl.style, { fontSize: '16px', lineHeight: '1.5' }); + content.appendChild(msgEl); + + const buttons = document.createElement('div'); + Object.assign(buttons.style, { display: 'flex', justifyContent: 'space-around', marginTop: '20px' }); + + const cancelBtn = document.createElement('button'); + cancelBtn.textContent = 'Cancelar'; + Object.assign(cancelBtn.style, { + padding: '10px 20px', fontSize: '16px', cursor: 'pointer', + backgroundColor: '#555', color: '#fff', border: '1px solid #888' + }); + cancelBtn.onclick = () => { this.parentContainer.removeChild(overlay); }; + buttons.appendChild(cancelBtn); + + const confirmBtn = document.createElement('button'); + confirmBtn.textContent = 'Aceptar'; + Object.assign(confirmBtn.style, { + padding: '10px 20px', fontSize: '16px', cursor: 'pointer', + backgroundColor: '#2a5', color: '#fff', border: '1px solid #888' + }); + confirmBtn.onclick = () => { + if (onConfirm) onConfirm(); + this.parentContainer.removeChild(overlay); + }; + buttons.appendChild(confirmBtn); + + content.appendChild(buttons); + overlay.appendChild(content); + this.parentContainer.appendChild(overlay); + } + + showTemporaryMessage(title, message, duration = 2000) { + const modal = document.createElement('div'); + Object.assign(modal.style, { + position: 'absolute', top: '25%', left: '50%', transform: 'translate(-50%, -50%)', + backgroundColor: 'rgba(139, 0, 0, 0.9)', color: '#fff', padding: '15px 30px', + borderRadius: '8px', border: '2px solid #ff4444', fontFamily: '"Cinzel", serif', + fontSize: '20px', textShadow: '2px 2px 4px black', zIndex: '2000', pointerEvents: 'none', + opacity: '0', transition: 'opacity 0.5s ease-in-out' + }); + + modal.innerHTML = ` +

⚠️ ${title}

+
${message}
+ `; + + document.body.appendChild(modal); + + requestAnimationFrame(() => { modal.style.opacity = '1'; }); + + setTimeout(() => { + modal.style.opacity = '0'; + setTimeout(() => { + if (modal.parentNode) document.body.removeChild(modal); + }, 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); + } +} diff --git a/src/view/ui/HUDManager.js b/src/view/ui/HUDManager.js new file mode 100644 index 0000000..ed09e17 --- /dev/null +++ b/src/view/ui/HUDManager.js @@ -0,0 +1,256 @@ +import { DIRECTIONS } from '../../engine/dungeon/Constants.js'; + +export class HUDManager { + constructor(gameContainer, cameraManager, game) { + this.parentContainer = gameContainer; + this.cameraManager = cameraManager; + this.game = game; // Needed for dungeon grid access (minimap) + + this.minimapCanvas = null; + this.zoomSlider = null; + this.viewButtons = []; + this.ctx = null; + + this.init(); + } + + init() { + // --- Minimap (Top Left) --- + this.minimapCanvas = document.createElement('canvas'); + this.minimapCanvas.width = 200; + this.minimapCanvas.height = 200; + Object.assign(this.minimapCanvas.style, { + position: 'absolute', + top: '10px', + left: '10px', + border: '2px solid #444', + backgroundColor: 'rgba(0, 0, 0, 0.8)', + pointerEvents: 'auto' + }); + this.parentContainer.appendChild(this.minimapCanvas); + this.ctx = this.minimapCanvas.getContext('2d'); + + // --- Camera Controls (Top Right) --- + const controlsContainer = document.createElement('div'); + Object.assign(controlsContainer.style, { + position: 'absolute', + top: '20px', + right: '20px', + display: 'flex', + gap: '10px', + alignItems: 'center', + pointerEvents: 'auto' + }); + this.parentContainer.appendChild(controlsContainer); + + this.createZoomControls(controlsContainer); + this.createViewControls(controlsContainer); + + // Start Minimap Loop + this.setupMinimapLoop(); + } + + createZoomControls(container) { + const zoomContainer = document.createElement('div'); + Object.assign(zoomContainer.style, { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: '0px', + height: '140px' + }); + + const zoomLabel = document.createElement('div'); + zoomLabel.textContent = 'Zoom'; + Object.assign(zoomLabel.style, { + color: '#fff', + fontSize: '15px', + fontFamily: 'sans-serif', + marginBottom: '10px', + marginTop: '0px' + }); + + const zoomSlider = document.createElement('input'); + zoomSlider.type = 'range'; + zoomSlider.min = '3'; + zoomSlider.max = '15'; + zoomSlider.value = '6'; + zoomSlider.step = '0.5'; + Object.assign(zoomSlider.style, { + width: '100px', + transform: 'rotate(-90deg)', + transformOrigin: 'center', + cursor: 'pointer', + marginTop: '40px' + }); + + this.zoomSlider = zoomSlider; + + // Sync with Camera Manager + this.cameraManager.zoomLevel = 6; + this.cameraManager.updateProjection(); + + this.cameraManager.onZoomChange = (val) => { + if (this.zoomSlider) this.zoomSlider.value = val; + }; + + zoomSlider.oninput = (e) => { + this.cameraManager.zoomLevel = parseFloat(e.target.value); + this.cameraManager.updateProjection(); + }; + + zoomContainer.appendChild(zoomLabel); + zoomContainer.appendChild(zoomSlider); + + // Add 2D/3D Toggle (Left of Zoom) + const toggleViewBtn = document.createElement('button'); + toggleViewBtn.textContent = '3D'; + toggleViewBtn.title = 'Cambiar vista 2D/3D'; + Object.assign(toggleViewBtn.style, { + width: '40px', + height: '40px', + borderRadius: '5px', + border: '1px solid #aaa', + backgroundColor: 'rgba(0, 0, 0, 0.6)', + color: '#daa520', + cursor: 'pointer', + fontFamily: '"Cinzel", serif', + fontWeight: 'bold', + fontSize: '14px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + }); + + toggleViewBtn.onmouseover = () => { toggleViewBtn.style.backgroundColor = 'rgba(0, 0, 0, 0.9)'; toggleViewBtn.style.color = '#fff'; }; + toggleViewBtn.onmouseout = () => { toggleViewBtn.style.backgroundColor = 'rgba(0, 0, 0, 0.6)'; toggleViewBtn.style.color = '#daa520'; }; + + toggleViewBtn.onclick = () => { + if (this.cameraManager) { + this.cameraManager.onAnimationComplete = null; + const isCurrently2D = (this.cameraManager.viewMode === '2D'); + if (isCurrently2D && this.cameraManager.renderer) { + this.cameraManager.renderer.hideTokens(); + } + + const is3D = this.cameraManager.toggleViewMode(); + toggleViewBtn.textContent = is3D ? '3D' : '2D'; + + if (!is3D) { + this.cameraManager.onAnimationComplete = () => { + if (this.cameraManager.renderer) { + this.cameraManager.renderer.showTokens(this.game.heroes, this.game.monsters); + } + }; + } + } + }; + + container.appendChild(toggleViewBtn); + container.appendChild(zoomContainer); + } + + createViewControls(container) { + const buttonsGrid = document.createElement('div'); + Object.assign(buttonsGrid.style, { + display: 'grid', + gridTemplateColumns: '40px 40px 40px', + gap: '5px' + }); + + const createBtn = (label, dir) => { + const btn = document.createElement('button'); + btn.textContent = label; + Object.assign(btn.style, { + width: '40px', + height: '40px', + backgroundColor: '#333', + color: '#fff', + border: '1px solid #666', + cursor: 'pointer', + transition: 'background-color 0.2s' + }); + btn.dataset.direction = dir; + btn.onclick = () => { + this.cameraManager.setIsoView(dir); + this.updateActiveViewButton(dir); + }; + return btn; + }; + + const btnN = createBtn('N', DIRECTIONS.NORTH); btnN.style.gridColumn = '2'; + const btnW = createBtn('W', DIRECTIONS.WEST); btnW.style.gridColumn = '1'; + const btnE = createBtn('E', DIRECTIONS.EAST); btnE.style.gridColumn = '3'; + const btnS = createBtn('S', DIRECTIONS.SOUTH); btnS.style.gridColumn = '2'; + + buttonsGrid.appendChild(btnN); + buttonsGrid.appendChild(btnW); + buttonsGrid.appendChild(btnE); + buttonsGrid.appendChild(btnS); + + this.viewButtons = [btnN, btnE, btnS, btnW]; + this.updateActiveViewButton(DIRECTIONS.NORTH); + + container.appendChild(buttonsGrid); + } + + updateActiveViewButton(activeDirection) { + this.viewButtons.forEach(btn => btn.style.backgroundColor = '#333'); + const activeBtn = this.viewButtons.find(btn => btn.dataset.direction === activeDirection); + if (activeBtn) activeBtn.style.backgroundColor = '#f0c040'; + } + + setupMinimapLoop() { + const loop = () => { + this.drawMinimap(); + requestAnimationFrame(loop); + }; + loop(); + } + + drawMinimap() { + if (!this.game.dungeon) return; + + const ctx = this.ctx; + const w = this.minimapCanvas.width; + const h = this.minimapCanvas.height; + + ctx.clearRect(0, 0, w, h); + + const cellSize = 5; + const centerX = w / 2; + const centerY = h / 2; + + ctx.fillStyle = '#666'; + + for (const [key, tileId] of this.game.dungeon.grid.occupiedCells) { + const [x, y] = key.split(',').map(Number); + const cx = centerX + (x * cellSize); + const cy = centerY - (y * cellSize); + + if (tileId.includes('room')) ctx.fillStyle = '#55a'; + else ctx.fillStyle = '#aaa'; + + ctx.fillRect(cx, cy, cellSize, cellSize); + } + + // Draw Exits + ctx.fillStyle = '#0f0'; + if (this.game.dungeon.availableExits) { + this.game.dungeon.availableExits.forEach(exit => { + const ex = centerX + (exit.x * cellSize); + const ey = centerY - (exit.y * cellSize); + ctx.fillRect(ex, ey, cellSize, cellSize); + }); + } + + // Draw Center Cross + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(centerX - 5, centerY); + ctx.lineTo(centerX + 5, centerY); + ctx.moveTo(centerX, centerY - 5); + ctx.lineTo(centerX, centerY + 5); + ctx.stroke(); + } +} diff --git a/src/view/ui/PlacementUI.js b/src/view/ui/PlacementUI.js new file mode 100644 index 0000000..e1535c9 --- /dev/null +++ b/src/view/ui/PlacementUI.js @@ -0,0 +1,140 @@ +export class PlacementUI { + constructor(parentContainer, game, callbacks) { + this.parentContainer = parentContainer; + this.game = game; // We need dynamic access to game.dungeon as it might change? Usually not. But we access game.dungeon. + this.callbacks = callbacks || {}; // { showModal, showConfirm } + + this.placementPanel = null; + this.placementStatus = null; + this.placeBtn = null; + this.rotateBtn = null; + this.discardBtn = null; + + this.init(); + } + + init() { + this.placementPanel = document.createElement('div'); + Object.assign(this.placementPanel.style, { + position: 'absolute', + bottom: '20px', + left: '50%', + transform: 'translateX(-50%)', + display: 'none', // Hidden by default + pointerEvents: 'auto', + backgroundColor: 'rgba(0, 0, 0, 0.85)', + padding: '15px', + borderRadius: '8px', + border: '2px solid #666' + }); + this.parentContainer.appendChild(this.placementPanel); + + // Status text + this.placementStatus = document.createElement('div'); + Object.assign(this.placementStatus.style, { + color: '#fff', fontSize: '16px', fontFamily: 'sans-serif', marginBottom: '10px', textAlign: 'center' + }); + this.placementStatus.textContent = 'Coloca la loseta'; + this.placementPanel.appendChild(this.placementStatus); + + // Controls container + const placementControls = document.createElement('div'); + Object.assign(placementControls.style, { display: 'flex', gap: '15px', alignItems: 'center' }); + this.placementPanel.appendChild(placementControls); + + // Movement arrows + const arrowGrid = document.createElement('div'); + Object.assign(arrowGrid.style, { display: 'grid', gridTemplateColumns: '40px 40px 40px', gap: '3px' }); + + const createArrow = (label, dx, dy) => { + const btn = document.createElement('button'); + btn.textContent = label; + Object.assign(btn.style, { + width: '40px', height: '40px', backgroundColor: '#444', color: '#fff', + border: '1px solid #888', cursor: 'pointer', fontSize: '18px' + }); + btn.onclick = () => { + if (this.game.dungeon) this.game.dungeon.movePlacement(dx, dy); + }; + return btn; + }; + + const arrowUp = createArrow('↑', 0, 1); arrowUp.style.gridColumn = '2'; + const arrowLeft = createArrow('←', -1, 0); arrowLeft.style.gridColumn = '1'; + const arrowRight = createArrow('→', 1, 0); arrowRight.style.gridColumn = '3'; + const arrowDown = createArrow('↓', 0, -1); arrowDown.style.gridColumn = '2'; + + arrowGrid.appendChild(arrowUp); + arrowGrid.appendChild(arrowLeft); + arrowGrid.appendChild(arrowRight); + arrowGrid.appendChild(arrowDown); + placementControls.appendChild(arrowGrid); + + // Rotate button + this.rotateBtn = document.createElement('button'); + this.rotateBtn.textContent = '🔄 Rotar'; + Object.assign(this.rotateBtn.style, { + padding: '10px 20px', backgroundColor: '#555', color: '#fff', border: '1px solid #888', + cursor: 'pointer', fontSize: '16px', borderRadius: '4px' + }); + this.rotateBtn.onclick = () => { if (this.game.dungeon) this.game.dungeon.rotatePlacement(); }; + placementControls.appendChild(this.rotateBtn); + + // Place button + this.placeBtn = document.createElement('button'); + this.placeBtn.textContent = '⬇ Bajar'; + Object.assign(this.placeBtn.style, { + padding: '10px 20px', backgroundColor: '#2a5', color: '#fff', border: '1px solid #888', + cursor: 'pointer', fontSize: '16px', borderRadius: '4px' + }); + this.placeBtn.onclick = () => { + if (this.game.dungeon) { + const success = this.game.dungeon.confirmPlacement(); + if (!success && this.callbacks.showModal) { + this.callbacks.showModal('Error de Colocación', 'No se puede colocar la loseta en esta posición.'); + } + } + }; + placementControls.appendChild(this.placeBtn); + + // Discard button + this.discardBtn = document.createElement('button'); + this.discardBtn.textContent = '❌ Cancelar'; + Object.assign(this.discardBtn.style, { + padding: '10px 20px', backgroundColor: '#d33', color: '#fff', border: '1px solid #888', + cursor: 'pointer', fontSize: '16px', borderRadius: '4px' + }); + this.discardBtn.onclick = () => { + if (this.game.dungeon && this.callbacks.showConfirm) { + this.callbacks.showConfirm( + 'Confirmar acción', + '¿Quieres descartar esta loseta y bloquear la puerta?', + () => { this.game.dungeon.cancelPlacement(); } + ); + } + }; + placementControls.appendChild(this.discardBtn); + } + + showControls(show) { + if (this.placementPanel) { + this.placementPanel.style.display = show ? 'block' : 'none'; + } + } + + updateStatus(isValid) { + if (this.placementStatus) { + if (isValid) { + this.placementStatus.textContent = '✅ Posición válida'; + this.placementStatus.style.color = '#0f0'; + this.placeBtn.style.backgroundColor = '#2a5'; + this.placeBtn.style.cursor = 'pointer'; + } else { + this.placementStatus.textContent = '❌ Posición inválida'; + this.placementStatus.style.color = '#f44'; + this.placeBtn.style.backgroundColor = '#555'; + this.placeBtn.style.cursor = 'not-allowed'; + } + } + } +} diff --git a/src/view/ui/SpellbookUI.js b/src/view/ui/SpellbookUI.js new file mode 100644 index 0000000..2135792 --- /dev/null +++ b/src/view/ui/SpellbookUI.js @@ -0,0 +1,110 @@ +import { SPELLS } from '../../engine/data/Spells.js'; + +export class SpellbookUI { + constructor(game) { + this.game = game; + this.spellBookContainer = null; + } + + toggle(hero) { + if (this.spellBookContainer) { + document.body.removeChild(this.spellBookContainer); + this.spellBookContainer = null; + return; + } + + const container = document.createElement('div'); + Object.assign(container.style, { + position: 'absolute', + bottom: '140px', + 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)' + }); + + 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); + + 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(); + document.body.removeChild(this.spellBookContainer); + this.spellBookContainer = null; + + if (spell.type === 'attack' || spell.type === 'defense') { + this.game.startSpellTargeting(spell); + } else { + // Global/Instant + this.game.executeSpell(spell); + } + }; + } + + // Cost Badge + 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 + const nameEl = document.createElement('div'); + nameEl.textContent = spell.name.toUpperCase(); + Object.assign(nameEl.style, { + position: 'absolute', top: '45px', width: '100%', textAlign: 'center', fontSize: '14px', + color: '#000', fontWeight: 'bold', fontFamily: '"Cinzel", serif', padding: '0 10px', boxSizing: 'border-box' + }); + card.appendChild(nameEl); + + // Description + 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; + } + + getSpellTemplate(type) { + 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}')`; + } +} diff --git a/src/view/ui/TurnStatusUI.js b/src/view/ui/TurnStatusUI.js new file mode 100644 index 0000000..3ddd110 --- /dev/null +++ b/src/view/ui/TurnStatusUI.js @@ -0,0 +1,172 @@ +export class TurnStatusUI { + constructor(parentContainer, game) { + this.parentContainer = parentContainer; + this.game = game; + + this.statusPanel = null; + this.phaseInfo = null; + this.endPhaseBtn = null; + this.notificationArea = null; + + this.init(); + } + + init() { + this.statusPanel = document.createElement('div'); + Object.assign(this.statusPanel.style, { + position: 'absolute', + top: '20px', + left: '50%', + transform: 'translateX(-50%)', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + pointerEvents: 'none' + }); + + // Turn/Phase Info + this.phaseInfo = document.createElement('div'); + Object.assign(this.phaseInfo.style, { + backgroundColor: 'rgba(0, 0, 0, 0.8)', + padding: '10px 20px', + border: '2px solid #daa520', + borderRadius: '5px', + color: '#fff', + fontFamily: '"Cinzel", serif', + fontSize: '20px', + textAlign: 'center', + textTransform: 'uppercase', + width: '300px' + }); + this.phaseInfo.innerHTML = ` +
Turn 1
+
Setup
+ `; + this.statusPanel.appendChild(this.phaseInfo); + + // End Phase Button + this.endPhaseBtn = document.createElement('button'); + this.endPhaseBtn.textContent = 'ACABAR FASE AVENTUREROS'; + Object.assign(this.endPhaseBtn.style, { + marginTop: '10px', + width: '300px', + padding: '8px', + backgroundColor: '#daa520', + color: '#000', + border: '1px solid #8B4513', + borderRadius: '3px', + fontWeight: 'bold', + cursor: 'pointer', + display: 'none', + fontFamily: '"Cinzel", serif', + fontSize: '12px', + pointerEvents: 'auto' + }); + + this.endPhaseBtn.onmouseover = () => { this.endPhaseBtn.style.backgroundColor = '#ffd700'; }; + this.endPhaseBtn.onmouseout = () => { this.endPhaseBtn.style.backgroundColor = '#daa520'; }; + + this.endPhaseBtn.onclick = () => { + console.log('[TurnStatusUI] End Phase Button Clicked', this.game.turnManager.currentPhase); + this.game.turnManager.nextPhase(); + }; + this.statusPanel.appendChild(this.endPhaseBtn); + + // Notification Area (Power Roll) + this.notificationArea = document.createElement('div'); + Object.assign(this.notificationArea.style, { + marginTop: '10px', + maxWidth: '600px', + transition: 'opacity 0.5s', + opacity: '0' + }); + this.statusPanel.appendChild(this.notificationArea); + + this.parentContainer.appendChild(this.statusPanel); + + // Inject Font + if (!document.getElementById('game-font')) { + const link = document.createElement('link'); + link.id = 'game-font'; + link.href = 'https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&display=swap'; + link.rel = 'stylesheet'; + document.head.appendChild(link); + } + } + + updatePhaseDisplay(phase, selectedHero) { + if (!this.phaseInfo) return; + const turn = this.game.turnManager.currentTurn; + + let content = ` +
Turn ${turn}
+
${phase.replace('_', ' ')}
+ `; + + if (selectedHero) { + content += this.getHeroStatsHTML(selectedHero); + } + + this.phaseInfo.innerHTML = content; + + if (this.endPhaseBtn) { + if (phase === 'hero') { + this.endPhaseBtn.style.display = 'block'; + this.endPhaseBtn.textContent = 'ACABAR FASE AVENTUREROS'; + this.endPhaseBtn.title = "Pasar a la Fase de Monstruos"; + } else if (phase === 'exploration') { + this.endPhaseBtn.style.display = 'block'; + this.endPhaseBtn.textContent = 'ACABAR TURNO'; + this.endPhaseBtn.title = "Finalizar turno y comenzar Fase de Poder"; + } else { + this.endPhaseBtn.style.display = 'none'; + } + } + } + + updateHeroStats(hero) { + const phase = this.game.turnManager.currentPhase; + this.updatePhaseDisplay(phase, hero); + } + + getHeroStatsHTML(hero) { + const portraitUrl = hero.texturePath || ''; + const lanternIcon = hero.hasLantern ? '🏮' : ''; + + return ` +
+
+ ${hero.name} +
+
+
+ ${hero.name} ${lanternIcon} +
+
+ Moves: ${hero.currentMoves} / ${hero.stats.move} +
+
+
+ `; + } + + showPowerRollResult(data) { + if (!this.notificationArea) return; + const { roll, message, eventTriggered } = data; + const color = eventTriggered ? '#ff4444' : '#44ff44'; + + this.notificationArea.innerHTML = ` +
+
Power Phase
+
${roll}
+
${message}
+
+ `; + + this.notificationArea.style.opacity = '1'; + + setTimeout(() => { + if (this.notificationArea) this.notificationArea.style.opacity = '0'; + }, 3000); + } +} diff --git a/src/view/ui/UnitCardManager.js b/src/view/ui/UnitCardManager.js new file mode 100644 index 0000000..eb114a8 --- /dev/null +++ b/src/view/ui/UnitCardManager.js @@ -0,0 +1,382 @@ +export class UnitCardManager { + constructor(parentContainer, game, callbacks) { + this.parentContainer = parentContainer; + this.game = game; + this.callbacks = callbacks || {}; // { showModal, toggleSpellBook } + + this.cardsContainer = null; + this.currentHeroCard = null; + this.currentMonsterCard = null; + this.placeholderCard = null; + this.attackButton = null; + + this.init(); + } + + init() { + this.cardsContainer = document.createElement('div'); + Object.assign(this.cardsContainer.style, { + position: 'absolute', + left: '10px', + top: '220px', // Below minimap + display: 'flex', + flexDirection: 'column', + gap: '10px', + pointerEvents: 'auto', + width: '200px' + }); + this.parentContainer.appendChild(this.cardsContainer); + + this.createPlaceholderCard(); + } + + createPlaceholderCard() { + const card = document.createElement('div'); + Object.assign(card.style, { + width: '180px', + height: '280px', + backgroundColor: 'rgba(20, 20, 20, 0.95)', + border: '2px solid #8B4513', + borderRadius: '8px', + padding: '10px', + fontFamily: '"Cinzel", serif', + color: '#888', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + textAlign: 'center' + }); + + const iconContainer = document.createElement('div'); + Object.assign(iconContainer.style, { + width: '100px', + height: '100px', + borderRadius: '50%', + border: '2px solid #8B4513', + backgroundColor: 'rgba(0, 0, 0, 0.5)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + marginBottom: '20px' + }); + + const icon = document.createElement('div'); + icon.textContent = '🎴'; + icon.style.fontSize = '48px'; + iconContainer.appendChild(icon); + card.appendChild(iconContainer); + + const text = document.createElement('div'); + text.textContent = 'Selecciona un Aventurero'; + text.style.fontSize = '14px'; + text.style.color = '#DAA520'; + card.appendChild(text); + + this.placeholderCard = card; + this.cardsContainer.appendChild(card); + } + + showHeroCard(hero) { + if (this.placeholderCard && this.placeholderCard.parentNode) { + this.cardsContainer.removeChild(this.placeholderCard); + } + if (this.currentHeroCard && this.currentHeroCard.parentNode) { + this.cardsContainer.removeChild(this.currentHeroCard); + } + + this.currentHeroCard = this.createHeroCard(hero); + this.cardsContainer.insertBefore(this.currentHeroCard, this.cardsContainer.firstChild); + } + + hideHeroCard() { + if (this.currentHeroCard && this.currentHeroCard.parentNode) { + this.cardsContainer.removeChild(this.currentHeroCard); + this.currentHeroCard = null; + } + // Show placeholder only if no cards are visible + if (!this.currentMonsterCard && this.placeholderCard && !this.placeholderCard.parentNode) { + this.cardsContainer.appendChild(this.placeholderCard); + } + } + + updateHeroCard(heroId) { + if (!this.currentHeroCard || this.currentHeroCard.dataset.heroId !== heroId) return; + + const hero = this.game.heroes.find(h => h.id === heroId); + if (!hero) return; + + const statsGrid = this.currentHeroCard.querySelector('div[style*="grid-template-columns"]'); + if (statsGrid) { + const statDivs = statsGrid.children; + // Assumed order: 4 -> Heridas, 7 -> Movimiento + if (statDivs[4]) { + const wValue = statDivs[4].querySelector('span:last-child'); + if (wValue) wValue.textContent = `${hero.currentWounds || hero.stats.wounds}/${hero.stats.wounds}`; + } + if (statDivs[7]) { + const movValue = statDivs[7].querySelector('span:last-child'); + if (movValue) movValue.textContent = `${hero.currentMoves || 0}/${hero.stats.move}`; + } + } + } + + createHeroCard(hero) { + const card = document.createElement('div'); + Object.assign(card.style, { + width: '180px', + backgroundColor: 'rgba(20, 20, 20, 0.95)', + border: '2px solid #8B4513', + borderRadius: '8px', + padding: '10px', + fontFamily: '"Cinzel", serif', + color: '#fff', + transition: 'all 0.3s', + cursor: 'pointer' + }); + + card.onmouseenter = () => { card.style.borderColor = '#DAA520'; card.style.transform = 'scale(1.05)'; }; + card.onmouseleave = () => { card.style.borderColor = '#8B4513'; card.style.transform = 'scale(1)'; }; + card.onclick = () => { if (this.game.onCellClick) this.game.onCellClick(hero.x, hero.y); }; + + // Portrait + const portrait = document.createElement('div'); + Object.assign(portrait.style, { + width: '100px', + height: '100px', + borderRadius: '50%', + overflow: 'hidden', + border: '2px solid #DAA520', + marginBottom: '8px', + marginLeft: 'auto', + marginRight: 'auto', + backgroundColor: '#000', + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + }); + + const tokenPath = `/assets/images/dungeon1/tokens/heroes/${hero.key}.png?v=2`; + const img = document.createElement('img'); + img.src = tokenPath; + Object.assign(img.style, { width: '100%', height: '100%', objectFit: 'cover' }); + img.onerror = () => { portrait.innerHTML = `
?
`; }; + portrait.appendChild(img); + card.appendChild(portrait); + + // Name + const name = document.createElement('div'); + name.textContent = hero.name; + Object.assign(name.style, { + fontSize: '16px', fontWeight: 'bold', color: '#DAA520', textAlign: 'center', marginBottom: '8px', textTransform: 'uppercase' + }); + card.appendChild(name); + + if (hero.hasLantern) { + const lantern = document.createElement('div'); + lantern.textContent = '🏮 Portador de la Lámpara'; + Object.assign(lantern.style, { fontSize: '10px', color: '#FFA500', textAlign: 'center', marginBottom: '8px' }); + card.appendChild(lantern); + } + + // Stats + const statsGrid = document.createElement('div'); + Object.assign(statsGrid.style, { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '4px', fontSize: '12px', marginBottom: '8px' }); + + const stats = [ + { label: 'H.C', value: hero.stats.ws || 0 }, + { label: 'H.P', value: hero.stats.bs || 0 }, + { label: 'Fuer', value: hero.stats.str || 0 }, + { label: 'Res', value: hero.stats.toughness || 0 }, + { label: 'Her', value: `${hero.currentWounds || hero.stats.wounds}/${hero.stats.wounds}` }, + { label: 'Ini', value: hero.stats.initiative || 0 }, + { label: 'Ata', value: hero.stats.attacks || 0 }, + { label: 'Mov', value: `${hero.currentMoves || 0}/${hero.stats.move}` } + ]; + + stats.forEach(stat => { + const el = document.createElement('div'); + Object.assign(el.style, { backgroundColor: 'rgba(0, 0, 0, 0.5)', padding: '3px 5px', borderRadius: '3px', display: 'flex', justifyContent: 'space-between' }); + + const l = document.createElement('span'); l.textContent = stat.label + ':'; l.style.color = '#AAA'; + const v = document.createElement('span'); v.textContent = stat.value; v.style.color = '#FFF'; v.style.fontWeight = 'bold'; + + el.appendChild(l); el.appendChild(v); + statsGrid.appendChild(el); + }); + card.appendChild(statsGrid); + + // Elf Bow Button + if (hero.key === 'elf') { + const isPinned = this.game.isEntityPinned(hero); + const hasAttacked = hero.hasAttacked; + const bowBtn = document.createElement('button'); + bowBtn.textContent = hasAttacked ? '🏹 YA DISPARADO' : '🏹 DISPARAR ARCO'; + Object.assign(bowBtn.style, { + width: '100%', padding: '8px', marginTop: '8px', + color: '#fff', border: '1px solid #fff', borderRadius: '4px', + fontFamily: '"Cinzel", serif', cursor: (isPinned || hasAttacked) ? 'not-allowed' : 'pointer', + backgroundColor: (isPinned || hasAttacked) ? '#555' : '#2E8B57' + }); + + if (isPinned) bowBtn.title = "¡Estás trabado en combate cuerpo a cuerpo!"; + else if (hasAttacked) bowBtn.title = "Ya has atacado en esta fase."; + else { + bowBtn.onclick = (e) => { + e.stopPropagation(); + this.game.startRangedTargeting(); + if (this.callbacks.showModal) this.callbacks.showModal('Modo Disparo', 'Selecciona un enemigo visible para disparar.'); + }; + } + card.appendChild(bowBtn); + } + + // Inventory + const invBtn = document.createElement('button'); + invBtn.textContent = '🎒 INVENTARIO'; + Object.assign(invBtn.style, { + width: '100%', padding: '8px', marginTop: '8px', backgroundColor: '#444', + color: '#fff', border: '1px solid #777', borderRadius: '4px', + fontFamily: '"Cinzel", serif', fontSize: '12px', cursor: 'not-allowed' + }); + invBtn.title = 'Inventario (Próximamente)'; + card.appendChild(invBtn); + + // Wizard Spells + if (hero.key === 'wizard') { + const spellsBtn = document.createElement('button'); + spellsBtn.textContent = '🔮 HECHIZOS'; + Object.assign(spellsBtn.style, { + width: '100%', padding: '8px', marginTop: '5px', backgroundColor: '#4b0082', + color: '#fff', border: '1px solid #8a2be2', borderRadius: '4px', + fontFamily: '"Cinzel", serif', cursor: 'pointer' + }); + spellsBtn.onclick = (e) => { + e.stopPropagation(); + if (this.callbacks.toggleSpellBook) this.callbacks.toggleSpellBook(hero); + }; + card.appendChild(spellsBtn); + } + + card.dataset.heroId = hero.id; + return card; + } + + createMonsterCard(monster) { + const card = document.createElement('div'); + Object.assign(card.style, { + width: '180px', backgroundColor: 'rgba(40, 20, 20, 0.95)', border: '2px solid #8B0000', + borderRadius: '8px', padding: '10px', fontFamily: '"Cinzel", serif', color: '#fff' + }); + + // Portrait + const portrait = document.createElement('div'); + Object.assign(portrait.style, { + width: '100px', height: '100px', borderRadius: '50%', overflow: 'hidden', + border: '2px solid #8B0000', marginBottom: '8px', marginLeft: 'auto', marginRight: 'auto', + backgroundColor: '#000', display: 'flex', alignItems: 'center', justifyContent: 'center' + }); + const img = document.createElement('img'); + img.src = `/assets/images/dungeon1/tokens/enemies/${monster.key}.png?v=2`; + Object.assign(img.style, { width: '100%', height: '100%', objectFit: 'cover' }); + img.onerror = () => { portrait.innerHTML = `
👹
`; }; + portrait.appendChild(img); + card.appendChild(portrait); + + // Name + const name = document.createElement('div'); + name.textContent = monster.name; + Object.assign(name.style, { + fontSize: '16px', fontWeight: 'bold', color: '#FF4444', textAlign: 'center', marginBottom: '8px', textTransform: 'uppercase' + }); + card.appendChild(name); + + // Stats + const statsGrid = document.createElement('div'); + Object.assign(statsGrid.style, { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '4px', fontSize: '12px' }); + const stats = [ + { label: 'H.C', value: monster.stats.ws || 0 }, + { label: 'Fuer', value: monster.stats.str || 0 }, + { label: 'Res', value: monster.stats.toughness || 0 }, + { label: 'Her', value: `${monster.currentWounds || monster.stats.wounds}/${monster.stats.wounds}` }, + { label: 'Ini', value: monster.stats.initiative || 0 }, + { label: 'Ata', value: monster.stats.attacks || 0 } + ]; + + stats.forEach(stat => { + const el = document.createElement('div'); + Object.assign(el.style, { backgroundColor: 'rgba(0, 0, 0, 0.5)', padding: '3px 5px', borderRadius: '3px', display: 'flex', justifyContent: 'space-between' }); + const l = document.createElement('span'); l.style.color = '#AAA'; l.textContent = stat.label + ':'; + const v = document.createElement('span'); v.style.color = '#FFF'; v.textContent = stat.value; v.style.fontWeight = 'bold'; + el.appendChild(l); el.appendChild(v); + statsGrid.appendChild(el); + }); + card.appendChild(statsGrid); + card.dataset.monsterId = monster.id; + return card; + } + + showMonsterCard(monster) { + this.hideMonsterCard(); + this.currentMonsterCard = this.createMonsterCard(monster); + this.cardsContainer.appendChild(this.currentMonsterCard); + + this.attackButton = document.createElement('button'); + this.attackButton.textContent = '⚔️ ATACAR'; + Object.assign(this.attackButton.style, { + width: '180px', padding: '12px', backgroundColor: '#8B0000', color: '#fff', + border: '2px solid #FF4444', borderRadius: '8px', fontFamily: '"Cinzel", serif', + fontSize: '16px', fontWeight: 'bold', cursor: 'pointer', transition: 'all 0.2s' + }); + + this.attackButton.onmouseenter = () => { this.attackButton.style.backgroundColor = '#FF0000'; this.attackButton.style.transform = 'scale(1.05)'; }; + this.attackButton.onmouseleave = () => { this.attackButton.style.backgroundColor = '#8B0000'; this.attackButton.style.transform = 'scale(1)'; }; + + this.attackButton.onclick = () => { + if (this.game.performHeroAttack) { + const result = this.game.performHeroAttack(monster.id); + if (result && result.success) { + this.hideMonsterCard(); + // Optional: deselect monster logic if managed externally + if (this.game.selectedMonster) { + if (this.game.onEntitySelect) this.game.onEntitySelect(this.game.selectedMonster.id, false); + this.game.selectedMonster = null; + } + } + } + }; + this.cardsContainer.appendChild(this.attackButton); + } + + showRangedAttackUI(monster) { + this.showMonsterCard(monster); // Creates button as "ATACAR" + + if (this.attackButton) { + this.attackButton.textContent = '🏹 DISPARAR'; + this.attackButton.style.backgroundColor = '#2E8B57'; + this.attackButton.style.border = '2px solid #32CD32'; + + this.attackButton.onclick = () => { + const result = this.game.performRangedAttack(monster.id); + if (result && result.success) { + this.game.cancelTargeting(); + this.hideMonsterCard(); + } + }; + + this.attackButton.onmouseenter = () => { this.attackButton.style.backgroundColor = '#3CB371'; this.attackButton.style.transform = 'scale(1.05)'; }; + this.attackButton.onmouseleave = () => { this.attackButton.style.backgroundColor = '#2E8B57'; this.attackButton.style.transform = 'scale(1)'; }; + } + } + + hideMonsterCard() { + if (this.currentMonsterCard && this.currentMonsterCard.parentNode) { + this.cardsContainer.removeChild(this.currentMonsterCard); + this.currentMonsterCard = null; + } + if (this.attackButton && this.attackButton.parentNode) { + this.cardsContainer.removeChild(this.attackButton); + this.attackButton = null; + } + } +}