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} ${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} ${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;
+ }
+ }
+}