feat: Sistema de combate completo con tarjetas de personajes y animaciones

- Tarjetas de héroes y monstruos con tokens circulares
- Sistema de selección: héroe + monstruo para atacar
- Botón de ATACAR en tarjeta de monstruo
- Animación de muerte: fade-out + hundimiento (1.5s)
- Visualización de estadísticas completas (WS, BS, S, T, W, I, A, Mov)
- Placeholder cuando no hay héroe seleccionado
- Tokens de héroes y monstruos en formato circular
- Deselección correcta de monstruos
- Fix: paso de gameEngine a CombatMechanics para callbacks de muerte
This commit is contained in:
2026-01-06 18:43:09 +01:00
parent 3efbf8d5fb
commit 7b28fcf1b0
22 changed files with 1513 additions and 25 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 KiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 960 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -0,0 +1 @@
goblin.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

View File

@@ -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);
}
}
}
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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) => {

View File

@@ -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;
}
}

View File

@@ -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 = `<div style="color: #DAA520; font-size: 48px;">?</div>`;
};
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 = `<div style="color: #8B0000; font-size: 48px;">👹</div>`;
};
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);

View File

@@ -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 = `<div style="color: #DAA520; font-size: 48px;">?</div>`;
};
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 = `<div style="font-size: 24px; color: #ff0000; font-weight:bold;">-${log.woundsCaused} HP</div>`;
} else {
detailHtml = `<div style="font-size: 20px; color: #aaa;">Sin Heridas (Armadura)</div>`;
}
} else {
detailHtml = `<div style="font-size: 18px; color: #888;">Esquivado / Fallado</div>`;
}
// Show simplified but impactful message
this.notificationArea.innerHTML = `
<div style="background-color: rgba(0,0,0,0.9); padding: 15px; border: 2px solid ${color}; border-radius: 5px; text-align: center; min-width: 250px;">
<div style="font-family: 'Cinzel'; font-size: 18px; color: ${color}; margin-bottom: 5px; text-transform:uppercase;">${log.attackerId.split('_')[0]} ATACA</div>
${detailHtml}
<div style="font-size: 14px; color: #ccc; margin-top:5px;">${log.message}</div>
</div>
`;
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 = `
<div style="font-size: 14px; color: #aaa;">Turn 1</div>
<div style="font-size: 24px; color: #daa520;">Setup</div>
`;
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 = `
<div style="font-size: 14px; color: #aaa;">Turn ${turn}</div>
<div style="font-size: 24px; color: #daa520;">${phase.replace('_', ' ')}</div>
`;
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 = `
<div style="font-size: 14px; color: #aaa;">Turn ${turn}</div>
<div style="font-size: 24px; color: #daa520;">${phase.replace('_', ' ')}</div>
`;
if (hero) {
content += this.getHeroStatsHTML(hero);
}
this.phaseInfo.innerHTML = content;
}
getHeroStatsHTML(hero) {
const portraitUrl = hero.texturePath || '';
const lanternIcon = hero.hasLantern ? '<span style="font-size: 20px; cursor: help;" title="Portador de la Lámpara">🏮</span>' : '';
return `
<div style="margin-top: 15px; border-top: 1px solid #555; paddingTop: 10px; display: flex; align-items: center; justify-content: center; gap: 15px;">
<div style="width: 50px; height: 50px; border-radius: 50%; overflow: hidden; border: 2px solid #daa520; background: #000;">
<img src="${portraitUrl}" style="width: 100%; height: 100%; object-fit: cover;" alt="${hero.name}">
</div>
<div style="text-align: left;">
<div style="color: #daa520; font-weight: bold; font-size: 16px;">
${hero.name} ${lanternIcon}
</div>
<div style="font-size: 14px;">
Moves: <span style="color: ${hero.currentMoves > 0 ? '#4f4' : '#f44'}; font-weight: bold;">${hero.currentMoves}</span> / ${hero.stats.move}
</div>
</div>
</div>
`;
}
showPowerRollResult(data) {
if (!this.notificationArea) return;
const { roll, message, eventTriggered } = data;
const color = eventTriggered ? '#ff4444' : '#44ff44';
this.notificationArea.innerHTML = `
<div style="background-color: rgba(0,0,0,0.9); padding: 15px; border: 1px solid ${color}; border-radius: 5px; text-align: center;">
<div style="font-family: 'Cinzel'; font-size: 18px; color: #fff; margin-bottom: 5px;">Power Phase</div>
<div style="font-size: 40px; font-weight: bold; color: ${color};">${roll}</div>
<div style="font-size: 14px; color: #ccc;">${message}</div>
</div>
`;
this.notificationArea.style.opacity = '1';
setTimeout(() => {
if (this.notificationArea) this.notificationArea.style.opacity = '0';
}, 3000);
}
}