diff --git a/public/assets/images/dungeon1/doors/door1_blocked.png b/public/assets/images/dungeon1/doors/door1_blocked.png
index 5da90c6..1b604ca 100644
Binary files a/public/assets/images/dungeon1/doors/door1_blocked.png and b/public/assets/images/dungeon1/doors/door1_blocked.png differ
diff --git a/public/assets/images/dungeon1/standees/enemies/Lordwarlock.png b/public/assets/images/dungeon1/standees/enemies/Lordwarlock.png
new file mode 100644
index 0000000..f429974
Binary files /dev/null and b/public/assets/images/dungeon1/standees/enemies/Lordwarlock.png differ
diff --git a/public/assets/images/dungeon1/standees/enemies/bat.png b/public/assets/images/dungeon1/standees/enemies/bat.png
new file mode 100644
index 0000000..6a322fc
Binary files /dev/null and b/public/assets/images/dungeon1/standees/enemies/bat.png differ
diff --git a/public/assets/images/dungeon1/standees/enemies/rat.png b/public/assets/images/dungeon1/standees/enemies/rat.png
new file mode 100644
index 0000000..c41283c
Binary files /dev/null and b/public/assets/images/dungeon1/standees/enemies/rat.png differ
diff --git a/public/assets/images/dungeon1/standees/enemies/skaven.png b/public/assets/images/dungeon1/standees/enemies/skaven.png
new file mode 100644
index 0000000..fbf0ec2
Binary files /dev/null and b/public/assets/images/dungeon1/standees/enemies/skaven.png differ
diff --git a/public/assets/images/dungeon1/standees/enemies/spiderGiant.png b/public/assets/images/dungeon1/standees/enemies/spiderGiant.png
new file mode 100644
index 0000000..b02f9e0
Binary files /dev/null and b/public/assets/images/dungeon1/standees/enemies/spiderGiant.png differ
diff --git a/public/assets/images/dungeon1/tokens/enemies/chaosWarrior.png b/public/assets/images/dungeon1/tokens/enemies/chaosWarrior.png
new file mode 100644
index 0000000..3b9294a
Binary files /dev/null and b/public/assets/images/dungeon1/tokens/enemies/chaosWarrior.png differ
diff --git a/public/assets/images/dungeon1/tokens/enemies/goblin.png b/public/assets/images/dungeon1/tokens/enemies/goblin.png
new file mode 100644
index 0000000..ea5451e
Binary files /dev/null and b/public/assets/images/dungeon1/tokens/enemies/goblin.png differ
diff --git a/public/assets/images/dungeon1/tokens/enemies/goblin_spearman.png b/public/assets/images/dungeon1/tokens/enemies/goblin_spearman.png
new file mode 120000
index 0000000..a25debe
--- /dev/null
+++ b/public/assets/images/dungeon1/tokens/enemies/goblin_spearman.png
@@ -0,0 +1 @@
+goblin.png
\ No newline at end of file
diff --git a/public/assets/images/dungeon1/tokens/enemies/orc.png b/public/assets/images/dungeon1/tokens/enemies/orc.png
new file mode 100644
index 0000000..9db50d9
Binary files /dev/null and b/public/assets/images/dungeon1/tokens/enemies/orc.png differ
diff --git a/public/assets/images/dungeon1/tokens/heroes/barbarian.png b/public/assets/images/dungeon1/tokens/heroes/barbarian.png
new file mode 100644
index 0000000..e20b614
Binary files /dev/null and b/public/assets/images/dungeon1/tokens/heroes/barbarian.png differ
diff --git a/public/assets/images/dungeon1/tokens/heroes/dwarf.png b/public/assets/images/dungeon1/tokens/heroes/dwarf.png
new file mode 100644
index 0000000..e6e8351
Binary files /dev/null and b/public/assets/images/dungeon1/tokens/heroes/dwarf.png differ
diff --git a/public/assets/images/dungeon1/tokens/heroes/elf.png b/public/assets/images/dungeon1/tokens/heroes/elf.png
new file mode 100644
index 0000000..22abb68
Binary files /dev/null and b/public/assets/images/dungeon1/tokens/heroes/elf.png differ
diff --git a/public/assets/images/dungeon1/tokens/heroes/wizard.png b/public/assets/images/dungeon1/tokens/heroes/wizard.png
new file mode 100644
index 0000000..5dff2ed
Binary files /dev/null and b/public/assets/images/dungeon1/tokens/heroes/wizard.png differ
diff --git a/public/assets/videos/Intro/intro_barbarian.mp4 b/public/assets/videos/Intro/intro_barbarian.mp4
new file mode 100644
index 0000000..0790c47
Binary files /dev/null and b/public/assets/videos/Intro/intro_barbarian.mp4 differ
diff --git a/src/engine/game/CombatMechanics.js b/src/engine/game/CombatMechanics.js
index bca3c2a..b43652b 100644
--- a/src/engine/game/CombatMechanics.js
+++ b/src/engine/game/CombatMechanics.js
@@ -20,7 +20,7 @@ export class CombatMechanics {
* @param {Object} defender
* @returns {Object} Result log
*/
- static resolveMeleeAttack(attacker, defender) {
+ static resolveMeleeAttack(attacker, defender, gameEngine = null) {
const log = {
attackerId: attacker.id,
defenderId: defender.id,
@@ -88,7 +88,7 @@ export class CombatMechanics {
}
// 6. Apply Damage to Defender State
- this.applyDamage(defender, wounds);
+ this.applyDamage(defender, wounds, gameEngine);
if (defender.isDead) {
log.defenderDied = true;
@@ -110,7 +110,7 @@ export class CombatMechanics {
return 6; // Fallback
}
- static applyDamage(entity, amount) {
+ static applyDamage(entity, amount, gameEngine = null) {
if (!entity.stats) entity.stats = {};
// If entity doesn't have current wounds tracked, init it from max
@@ -135,6 +135,10 @@ export class CombatMechanics {
if (entity.currentWounds <= 0) {
entity.currentWounds = 0;
entity.isDead = true;
+ // Trigger death callback if available
+ if (gameEngine && gameEngine.onEntityDeath) {
+ gameEngine.onEntityDeath(entity.id);
+ }
}
}
}
diff --git a/src/engine/game/GameEngine.js b/src/engine/game/GameEngine.js
index 7b10916..57079e3 100644
--- a/src/engine/game/GameEngine.js
+++ b/src/engine/game/GameEngine.js
@@ -27,6 +27,7 @@ export class GameEngine {
this.onEntitySelect = null;
this.onEntityActive = null; // New: When entity starts/ends turn
this.onEntityHit = null; // New: When entity takes damage
+ this.onEntityDeath = null; // New: When entity dies
this.onPathChange = null;
}
@@ -149,29 +150,42 @@ export class GameEngine {
const clickedHero = this.heroes.find(h => h.x === x && h.y === y);
const clickedMonster = this.monsters ? this.monsters.find(m => m.x === x && m.y === y && !m.isDead) : null;
- // COMBAT: Hero Attack Check
- if (clickedMonster && this.selectedEntity && this.selectedEntity.type === 'hero') {
- const attackResult = this.performHeroAttack(clickedMonster.id);
- if (attackResult && attackResult.success) {
- // Attack performed, do not deselect hero
- return;
- }
- // If attack failed (e.g. not adjacent), proceeds to select the monster
- }
-
const clickedEntity = clickedHero || clickedMonster;
if (clickedEntity) {
if (this.selectedEntity === clickedEntity) {
// Toggle Deselect
this.deselectEntity();
- } else {
- // Select new entity
- if (this.selectedEntity) this.deselectEntity();
-
- this.selectedEntity = clickedEntity;
+ } else if (this.selectedMonster === clickedMonster && clickedMonster) {
+ // Clicking on already selected monster - deselect it
+ const monsterId = this.selectedMonster.id;
+ this.selectedMonster = null;
if (this.onEntitySelect) {
- this.onEntitySelect(clickedEntity.id, true);
+ this.onEntitySelect(monsterId, false);
+ }
+ } else {
+ // Select new entity (don't deselect hero if clicking monster)
+ if (clickedMonster && this.selectedEntity && this.selectedEntity.type === 'hero') {
+ // Deselect previous monster if any
+ if (this.selectedMonster) {
+ const prevMonsterId = this.selectedMonster.id;
+ if (this.onEntitySelect) {
+ this.onEntitySelect(prevMonsterId, false);
+ }
+ }
+ // Keep hero selected, also select monster
+ this.selectedMonster = clickedMonster;
+ if (this.onEntitySelect) {
+ this.onEntitySelect(clickedMonster.id, true);
+ }
+ } else {
+ // Normal selection (deselect previous)
+ if (this.selectedEntity) this.deselectEntity();
+
+ this.selectedEntity = clickedEntity;
+ if (this.onEntitySelect) {
+ this.onEntitySelect(clickedEntity.id, true);
+ }
}
}
return;
@@ -201,7 +215,7 @@ export class GameEngine {
if (hero.hasAttacked) return { success: false, reason: 'cooldown' };
// Execute Attack
- const result = CombatMechanics.resolveMeleeAttack(hero, monster);
+ const result = CombatMechanics.resolveMeleeAttack(hero, monster, this);
hero.hasAttacked = true;
if (this.onCombatResult) this.onCombatResult(result);
@@ -216,6 +230,13 @@ export class GameEngine {
this.plannedPath = [];
if (this.onEntitySelect) this.onEntitySelect(id, false);
if (this.onPathChange) this.onPathChange([]);
+
+ // Also deselect monster if selected
+ if (this.selectedMonster) {
+ const monsterId = this.selectedMonster.id;
+ this.selectedMonster = null;
+ if (this.onEntitySelect) this.onEntitySelect(monsterId, false);
+ }
}
// Alias for legacy calls if any
diff --git a/src/engine/game/MonsterAI.js b/src/engine/game/MonsterAI.js
index 41a7a18..5daf4c7 100644
--- a/src/engine/game/MonsterAI.js
+++ b/src/engine/game/MonsterAI.js
@@ -220,7 +220,7 @@ export class MonsterAI {
// 4. Remove both rings
// 5. Show combat result
- const result = CombatMechanics.resolveMeleeAttack(monster, hero);
+ const result = CombatMechanics.resolveMeleeAttack(monster, hero, this.game);
// Step 1: Green ring on attacker
if (this.game.onEntityActive) {
diff --git a/src/main.js b/src/main.js
index 5bc2634..d0b1b32 100644
--- a/src/main.js
+++ b/src/main.js
@@ -111,6 +111,10 @@ game.onEntityHit = (entityId) => {
renderer.triggerDamageEffect(entityId);
};
+game.onEntityDeath = (entityId) => {
+ renderer.triggerDeathAnimation(entityId);
+};
+
// game.onEntitySelect is now handled by UIManager to wrap the renderer call
renderer.onHeroFinishedMove = (x, y) => {
diff --git a/src/view/GameRenderer.js b/src/view/GameRenderer.js
index 5447ef4..fcd87d2 100644
--- a/src/view/GameRenderer.js
+++ b/src/view/GameRenderer.js
@@ -290,6 +290,32 @@ export class GameRenderer {
};
}
+ triggerDeathAnimation(entityId) {
+ const mesh = this.entities.get(entityId);
+ if (!mesh) return;
+
+ console.log(`[GameRenderer] Triggering death animation for ${entityId}`);
+
+ // Start fade-out animation
+ const startTime = performance.now();
+ const duration = 1500; // 1.5 seconds fade out
+
+ mesh.userData.death = {
+ startTime: startTime,
+ duration: duration,
+ initialOpacity: 1.0
+ };
+
+ // Remove entity from map after animation completes
+ setTimeout(() => {
+ if (mesh && mesh.parent) {
+ mesh.parent.remove(mesh);
+ }
+ this.entities.delete(entityId);
+ console.log(`[GameRenderer] Removed entity ${entityId} from scene`);
+ }, duration);
+ }
+
moveEntityAlongPath(entity, path) {
const mesh = this.entities.get(entity.id);
if (mesh) {
@@ -370,6 +396,38 @@ export class GameRenderer {
mesh.position.copy(data.shake.originalPos);
delete data.shake;
}
+ } else if (data.death) {
+ // HANDLE DEATH FADE-OUT
+ const elapsed = time - data.death.startTime;
+ const progress = Math.min(elapsed / data.death.duration, 1);
+
+ // Fade out opacity
+ const opacity = data.death.initialOpacity * (1 - progress);
+
+ // Apply opacity to all materials in the mesh
+ mesh.traverse((child) => {
+ if (child.material) {
+ if (Array.isArray(child.material)) {
+ child.material.forEach(mat => {
+ mat.transparent = true;
+ mat.opacity = opacity;
+ });
+ } else {
+ child.material.transparent = true;
+ child.material.opacity = opacity;
+ }
+ }
+ });
+
+ // Also fade down (sink into ground)
+ if (data.death.initialY === undefined) {
+ data.death.initialY = mesh.position.y;
+ }
+ mesh.position.y = data.death.initialY - (progress * 0.5);
+
+ if (progress >= 1) {
+ delete data.death;
+ }
}
diff --git a/src/view/UIManager.js b/src/view/UIManager.js
index e64b727..efc36e2 100644
--- a/src/view/UIManager.js
+++ b/src/view/UIManager.js
@@ -8,6 +8,7 @@ export class UIManager {
this.selectedHero = null;
this.createHUD();
+ this.createHeroCardsPanel(); // NEW: Hero stat cards
this.createGameStatusPanel(); // New Panel
this.setupMinimapLoop();
this.setupGameListeners(); // New Listeners
@@ -25,11 +26,28 @@ export class UIManager {
// 2. Update UI
if (isSelected) {
const hero = this.game.heroes.find(h => h.id === id);
- this.selectedHero = hero; // Store state
- this.updateHeroStats(hero);
+ const monster = this.game.monsters ? this.game.monsters.find(m => m.id === id) : null;
+
+ 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);
+ }
} else {
- this.selectedHero = null;
- this.updateHeroStats(null);
+ // Deselection - check what type was deselected
+ if (this.selectedHero && this.selectedHero.id === id) {
+ // Hero was deselected
+ this.selectedHero = null;
+ this.updateHeroStats(null);
+ this.hideHeroCard();
+ } else {
+ // Monster was deselected
+ this.hideMonsterCard();
+ }
}
};
@@ -37,6 +55,10 @@ export class UIManager {
this.game.onEntityMove = (entity, path) => {
if (originalMove) originalMove(entity, path);
this.updateHeroStats(entity);
+ // Update hero card if it's a hero
+ if (entity.type === 'hero') {
+ this.updateHeroCard(entity.id);
+ }
};
}
@@ -303,6 +325,412 @@ export class UIManager {
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: 'WS', value: hero.stats.ws || 0 },
+ { label: 'BS', value: hero.stats.bs || 0 },
+ { label: 'S', value: hero.stats.str || 0 },
+ { label: 'T', value: hero.stats.toughness || 0 },
+ { label: 'W', value: `${hero.currentWounds || hero.stats.wounds}/${hero.stats.wounds}` },
+ { label: 'I', value: hero.stats.initiative || 0 },
+ { label: 'A', value: hero.stats.attacks || 0 },
+ { label: 'Mov', value: `${hero.currentMoves || 0}/${hero.stats.move}` }
+ ];
+
+ 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.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: 'WS', value: monster.stats.ws || 0 },
+ { label: 'S', value: monster.stats.str || 0 },
+ { label: 'T', value: monster.stats.toughness || 0 },
+ { label: 'W', value: `${monster.currentWounds || monster.stats.wounds}/${monster.stats.wounds}` },
+ { label: 'I', value: monster.stats.initiative || 0 },
+ { label: 'A', value: monster.stats.attacks || 0 }
+ ];
+
+ 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);
+ }
+
+ 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';
@@ -479,6 +907,13 @@ export class UIManager {
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);
diff --git a/src/view/UIManager.js.backup b/src/view/UIManager.js.backup
new file mode 100644
index 0000000..83f1656
--- /dev/null
+++ b/src/view/UIManager.js.backup
@@ -0,0 +1,965 @@
+import { DIRECTIONS } from '../engine/dungeon/Constants.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
+
+ // Hook into engine callbacks for UI updates
+ const originalSelect = this.game.onEntitySelect;
+ this.game.onEntitySelect = (id, isSelected) => {
+ // 1. Call Renderer (was in main.js)
+ if (this.cameraManager && this.cameraManager.renderer) {
+ this.cameraManager.renderer.toggleEntitySelection(id, isSelected);
+ } else if (window.RENDERER) {
+ window.RENDERER.toggleEntitySelection(id, isSelected);
+ }
+
+ // 2. Update UI
+ if (isSelected) {
+ const hero = this.game.heroes.find(h => h.id === id);
+ this.selectedHero = hero; // Store state
+ this.updateHeroStats(hero);
+ } else {
+ this.selectedHero = null;
+ this.updateHeroStats(null);
+ }
+ };
+
+ 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
+ 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);
+
+ // Direction buttons grid
+ const buttonsGrid = document.createElement('div');
+ buttonsGrid.style.display = 'grid';
+ buttonsGrid.style.gridTemplateColumns = '40px 40px 40px';
+ buttonsGrid.style.gap = '5px';
+
+ 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';
+
+ const icon = document.createElement('div');
+ icon.textContent = '🎴';
+ icon.style.fontSize = '64px';
+ icon.style.marginBottom = '20px';
+ card.appendChild(icon);
+
+ 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
+ const portrait = document.createElement('div');
+ portrait.style.width = '100%';
+ portrait.style.height = '100px';
+ portrait.style.borderRadius = '5px';
+ portrait.style.overflow = 'hidden';
+ portrait.style.border = '2px solid #DAA520';
+ portrait.style.marginBottom = '8px';
+ 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`;
+ 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: 'WS', value: hero.stats.ws || 0 },
+ { label: 'BS', value: hero.stats.bs || 0 },
+ { label: 'S', value: hero.stats.str || 0 },
+ { label: 'T', value: hero.stats.toughness || 0 },
+ { label: 'W', value: `${hero.currentWounds || hero.stats.wounds}/${hero.stats.wounds}` },
+ { label: 'I', value: hero.stats.initiative || 0 },
+ { label: 'A', value: hero.stats.attacks || 0 },
+ { label: 'Mov', value: `${hero.currentMoves || 0}/${hero.stats.move}` }
+ ];
+
+ 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);
+
+ // Store reference
+ this.heroCards.set(hero.id, card);
+ this.heroCardsContainer.appendChild(card);
+ }
+
+ updateHeroCard(heroId) {
+ const card = this.heroCards.get(heroId);
+ if (!card) return;
+
+ const hero = this.game.heroes.find(h => h.id === heroId);
+ if (!hero) return;
+
+ // Update wounds and moves in the stats grid
+ const statsGrid = card.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}`;
+ }
+ }
+ }
+
+ 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.minWidth = '200px';
+ 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 = '100%';
+ 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.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() {
+ if (this.game.turnManager) {
+ this.game.turnManager.on('phase_changed', (phase) => {
+ this.updatePhaseDisplay(phase);
+ });
+
+ this.game.turnManager.on('POWER_RESULT', (data) => {
+ this.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 === 'monster') {
+ this.endPhaseBtn.style.display = 'block';
+ this.endPhaseBtn.textContent = 'ACABAR FASE MONSTRUOS';
+ this.endPhaseBtn.title = "Pasar a Fase de Exploración";
+ } 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);
+ }
+}