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
|
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 1.3 MiB |
BIN
public/assets/images/dungeon1/standees/enemies/Lordwarlock.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
public/assets/images/dungeon1/standees/enemies/bat.png
Normal file
|
After Width: | Height: | Size: 960 KiB |
BIN
public/assets/images/dungeon1/standees/enemies/rat.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/assets/images/dungeon1/standees/enemies/skaven.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
public/assets/images/dungeon1/standees/enemies/spiderGiant.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
public/assets/images/dungeon1/tokens/enemies/chaosWarrior.png
Normal file
|
After Width: | Height: | Size: 259 KiB |
BIN
public/assets/images/dungeon1/tokens/enemies/goblin.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
1
public/assets/images/dungeon1/tokens/enemies/goblin_spearman.png
Symbolic link
@@ -0,0 +1 @@
|
||||
goblin.png
|
||||
BIN
public/assets/images/dungeon1/tokens/enemies/orc.png
Normal file
|
After Width: | Height: | Size: 306 KiB |
BIN
public/assets/images/dungeon1/tokens/heroes/barbarian.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
public/assets/images/dungeon1/tokens/heroes/dwarf.png
Normal file
|
After Width: | Height: | Size: 388 KiB |
BIN
public/assets/images/dungeon1/tokens/heroes/elf.png
Normal file
|
After Width: | Height: | Size: 199 KiB |
BIN
public/assets/images/dungeon1/tokens/heroes/wizard.png
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
public/assets/videos/Intro/intro_barbarian.mp4
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
965
src/view/UIManager.js.backup
Normal 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);
|
||||
}
|
||||
}
|
||||