diff --git a/public/assets/images/dungeon1/doors/door1_blocked.png b/public/assets/images/dungeon1/doors/door1_blocked.png index 5da90c6..1b604ca 100644 Binary files a/public/assets/images/dungeon1/doors/door1_blocked.png and b/public/assets/images/dungeon1/doors/door1_blocked.png differ diff --git a/public/assets/images/dungeon1/standees/enemies/Lordwarlock.png b/public/assets/images/dungeon1/standees/enemies/Lordwarlock.png new file mode 100644 index 0000000..f429974 Binary files /dev/null and b/public/assets/images/dungeon1/standees/enemies/Lordwarlock.png differ diff --git a/public/assets/images/dungeon1/standees/enemies/bat.png b/public/assets/images/dungeon1/standees/enemies/bat.png new file mode 100644 index 0000000..6a322fc Binary files /dev/null and b/public/assets/images/dungeon1/standees/enemies/bat.png differ diff --git a/public/assets/images/dungeon1/standees/enemies/rat.png b/public/assets/images/dungeon1/standees/enemies/rat.png new file mode 100644 index 0000000..c41283c Binary files /dev/null and b/public/assets/images/dungeon1/standees/enemies/rat.png differ diff --git a/public/assets/images/dungeon1/standees/enemies/skaven.png b/public/assets/images/dungeon1/standees/enemies/skaven.png new file mode 100644 index 0000000..fbf0ec2 Binary files /dev/null and b/public/assets/images/dungeon1/standees/enemies/skaven.png differ diff --git a/public/assets/images/dungeon1/standees/enemies/spiderGiant.png b/public/assets/images/dungeon1/standees/enemies/spiderGiant.png new file mode 100644 index 0000000..b02f9e0 Binary files /dev/null and b/public/assets/images/dungeon1/standees/enemies/spiderGiant.png differ diff --git a/public/assets/images/dungeon1/tokens/enemies/chaosWarrior.png b/public/assets/images/dungeon1/tokens/enemies/chaosWarrior.png new file mode 100644 index 0000000..3b9294a Binary files /dev/null and b/public/assets/images/dungeon1/tokens/enemies/chaosWarrior.png differ diff --git a/public/assets/images/dungeon1/tokens/enemies/goblin.png b/public/assets/images/dungeon1/tokens/enemies/goblin.png new file mode 100644 index 0000000..ea5451e Binary files /dev/null and b/public/assets/images/dungeon1/tokens/enemies/goblin.png differ diff --git a/public/assets/images/dungeon1/tokens/enemies/goblin_spearman.png b/public/assets/images/dungeon1/tokens/enemies/goblin_spearman.png new file mode 120000 index 0000000..a25debe --- /dev/null +++ b/public/assets/images/dungeon1/tokens/enemies/goblin_spearman.png @@ -0,0 +1 @@ +goblin.png \ No newline at end of file diff --git a/public/assets/images/dungeon1/tokens/enemies/orc.png b/public/assets/images/dungeon1/tokens/enemies/orc.png new file mode 100644 index 0000000..9db50d9 Binary files /dev/null and b/public/assets/images/dungeon1/tokens/enemies/orc.png differ diff --git a/public/assets/images/dungeon1/tokens/heroes/barbarian.png b/public/assets/images/dungeon1/tokens/heroes/barbarian.png new file mode 100644 index 0000000..e20b614 Binary files /dev/null and b/public/assets/images/dungeon1/tokens/heroes/barbarian.png differ diff --git a/public/assets/images/dungeon1/tokens/heroes/dwarf.png b/public/assets/images/dungeon1/tokens/heroes/dwarf.png new file mode 100644 index 0000000..e6e8351 Binary files /dev/null and b/public/assets/images/dungeon1/tokens/heroes/dwarf.png differ diff --git a/public/assets/images/dungeon1/tokens/heroes/elf.png b/public/assets/images/dungeon1/tokens/heroes/elf.png new file mode 100644 index 0000000..22abb68 Binary files /dev/null and b/public/assets/images/dungeon1/tokens/heroes/elf.png differ diff --git a/public/assets/images/dungeon1/tokens/heroes/wizard.png b/public/assets/images/dungeon1/tokens/heroes/wizard.png new file mode 100644 index 0000000..5dff2ed Binary files /dev/null and b/public/assets/images/dungeon1/tokens/heroes/wizard.png differ diff --git a/public/assets/videos/Intro/intro_barbarian.mp4 b/public/assets/videos/Intro/intro_barbarian.mp4 new file mode 100644 index 0000000..0790c47 Binary files /dev/null and b/public/assets/videos/Intro/intro_barbarian.mp4 differ diff --git a/src/engine/game/CombatMechanics.js b/src/engine/game/CombatMechanics.js index bca3c2a..b43652b 100644 --- a/src/engine/game/CombatMechanics.js +++ b/src/engine/game/CombatMechanics.js @@ -20,7 +20,7 @@ export class CombatMechanics { * @param {Object} defender * @returns {Object} Result log */ - static resolveMeleeAttack(attacker, defender) { + static resolveMeleeAttack(attacker, defender, gameEngine = null) { const log = { attackerId: attacker.id, defenderId: defender.id, @@ -88,7 +88,7 @@ export class CombatMechanics { } // 6. Apply Damage to Defender State - this.applyDamage(defender, wounds); + this.applyDamage(defender, wounds, gameEngine); if (defender.isDead) { log.defenderDied = true; @@ -110,7 +110,7 @@ export class CombatMechanics { return 6; // Fallback } - static applyDamage(entity, amount) { + static applyDamage(entity, amount, gameEngine = null) { if (!entity.stats) entity.stats = {}; // If entity doesn't have current wounds tracked, init it from max @@ -135,6 +135,10 @@ export class CombatMechanics { if (entity.currentWounds <= 0) { entity.currentWounds = 0; entity.isDead = true; + // Trigger death callback if available + if (gameEngine && gameEngine.onEntityDeath) { + gameEngine.onEntityDeath(entity.id); + } } } } diff --git a/src/engine/game/GameEngine.js b/src/engine/game/GameEngine.js index 7b10916..57079e3 100644 --- a/src/engine/game/GameEngine.js +++ b/src/engine/game/GameEngine.js @@ -27,6 +27,7 @@ export class GameEngine { this.onEntitySelect = null; this.onEntityActive = null; // New: When entity starts/ends turn this.onEntityHit = null; // New: When entity takes damage + this.onEntityDeath = null; // New: When entity dies this.onPathChange = null; } @@ -149,29 +150,42 @@ export class GameEngine { const clickedHero = this.heroes.find(h => h.x === x && h.y === y); const clickedMonster = this.monsters ? this.monsters.find(m => m.x === x && m.y === y && !m.isDead) : null; - // COMBAT: Hero Attack Check - if (clickedMonster && this.selectedEntity && this.selectedEntity.type === 'hero') { - const attackResult = this.performHeroAttack(clickedMonster.id); - if (attackResult && attackResult.success) { - // Attack performed, do not deselect hero - return; - } - // If attack failed (e.g. not adjacent), proceeds to select the monster - } - const clickedEntity = clickedHero || clickedMonster; if (clickedEntity) { if (this.selectedEntity === clickedEntity) { // Toggle Deselect this.deselectEntity(); - } else { - // Select new entity - if (this.selectedEntity) this.deselectEntity(); - - this.selectedEntity = clickedEntity; + } else if (this.selectedMonster === clickedMonster && clickedMonster) { + // Clicking on already selected monster - deselect it + const monsterId = this.selectedMonster.id; + this.selectedMonster = null; if (this.onEntitySelect) { - this.onEntitySelect(clickedEntity.id, true); + this.onEntitySelect(monsterId, false); + } + } else { + // Select new entity (don't deselect hero if clicking monster) + if (clickedMonster && this.selectedEntity && this.selectedEntity.type === 'hero') { + // Deselect previous monster if any + if (this.selectedMonster) { + const prevMonsterId = this.selectedMonster.id; + if (this.onEntitySelect) { + this.onEntitySelect(prevMonsterId, false); + } + } + // Keep hero selected, also select monster + this.selectedMonster = clickedMonster; + if (this.onEntitySelect) { + this.onEntitySelect(clickedMonster.id, true); + } + } else { + // Normal selection (deselect previous) + if (this.selectedEntity) this.deselectEntity(); + + this.selectedEntity = clickedEntity; + if (this.onEntitySelect) { + this.onEntitySelect(clickedEntity.id, true); + } } } return; @@ -201,7 +215,7 @@ export class GameEngine { if (hero.hasAttacked) return { success: false, reason: 'cooldown' }; // Execute Attack - const result = CombatMechanics.resolveMeleeAttack(hero, monster); + const result = CombatMechanics.resolveMeleeAttack(hero, monster, this); hero.hasAttacked = true; if (this.onCombatResult) this.onCombatResult(result); @@ -216,6 +230,13 @@ export class GameEngine { this.plannedPath = []; if (this.onEntitySelect) this.onEntitySelect(id, false); if (this.onPathChange) this.onPathChange([]); + + // Also deselect monster if selected + if (this.selectedMonster) { + const monsterId = this.selectedMonster.id; + this.selectedMonster = null; + if (this.onEntitySelect) this.onEntitySelect(monsterId, false); + } } // Alias for legacy calls if any diff --git a/src/engine/game/MonsterAI.js b/src/engine/game/MonsterAI.js index 41a7a18..5daf4c7 100644 --- a/src/engine/game/MonsterAI.js +++ b/src/engine/game/MonsterAI.js @@ -220,7 +220,7 @@ export class MonsterAI { // 4. Remove both rings // 5. Show combat result - const result = CombatMechanics.resolveMeleeAttack(monster, hero); + const result = CombatMechanics.resolveMeleeAttack(monster, hero, this.game); // Step 1: Green ring on attacker if (this.game.onEntityActive) { diff --git a/src/main.js b/src/main.js index 5bc2634..d0b1b32 100644 --- a/src/main.js +++ b/src/main.js @@ -111,6 +111,10 @@ game.onEntityHit = (entityId) => { renderer.triggerDamageEffect(entityId); }; +game.onEntityDeath = (entityId) => { + renderer.triggerDeathAnimation(entityId); +}; + // game.onEntitySelect is now handled by UIManager to wrap the renderer call renderer.onHeroFinishedMove = (x, y) => { diff --git a/src/view/GameRenderer.js b/src/view/GameRenderer.js index 5447ef4..fcd87d2 100644 --- a/src/view/GameRenderer.js +++ b/src/view/GameRenderer.js @@ -290,6 +290,32 @@ export class GameRenderer { }; } + triggerDeathAnimation(entityId) { + const mesh = this.entities.get(entityId); + if (!mesh) return; + + console.log(`[GameRenderer] Triggering death animation for ${entityId}`); + + // Start fade-out animation + const startTime = performance.now(); + const duration = 1500; // 1.5 seconds fade out + + mesh.userData.death = { + startTime: startTime, + duration: duration, + initialOpacity: 1.0 + }; + + // Remove entity from map after animation completes + setTimeout(() => { + if (mesh && mesh.parent) { + mesh.parent.remove(mesh); + } + this.entities.delete(entityId); + console.log(`[GameRenderer] Removed entity ${entityId} from scene`); + }, duration); + } + moveEntityAlongPath(entity, path) { const mesh = this.entities.get(entity.id); if (mesh) { @@ -370,6 +396,38 @@ export class GameRenderer { mesh.position.copy(data.shake.originalPos); delete data.shake; } + } else if (data.death) { + // HANDLE DEATH FADE-OUT + const elapsed = time - data.death.startTime; + const progress = Math.min(elapsed / data.death.duration, 1); + + // Fade out opacity + const opacity = data.death.initialOpacity * (1 - progress); + + // Apply opacity to all materials in the mesh + mesh.traverse((child) => { + if (child.material) { + if (Array.isArray(child.material)) { + child.material.forEach(mat => { + mat.transparent = true; + mat.opacity = opacity; + }); + } else { + child.material.transparent = true; + child.material.opacity = opacity; + } + } + }); + + // Also fade down (sink into ground) + if (data.death.initialY === undefined) { + data.death.initialY = mesh.position.y; + } + mesh.position.y = data.death.initialY - (progress * 0.5); + + if (progress >= 1) { + delete data.death; + } } diff --git a/src/view/UIManager.js b/src/view/UIManager.js index e64b727..efc36e2 100644 --- a/src/view/UIManager.js +++ b/src/view/UIManager.js @@ -8,6 +8,7 @@ export class UIManager { this.selectedHero = null; this.createHUD(); + this.createHeroCardsPanel(); // NEW: Hero stat cards this.createGameStatusPanel(); // New Panel this.setupMinimapLoop(); this.setupGameListeners(); // New Listeners @@ -25,11 +26,28 @@ export class UIManager { // 2. Update UI if (isSelected) { const hero = this.game.heroes.find(h => h.id === id); - this.selectedHero = hero; // Store state - this.updateHeroStats(hero); + const monster = this.game.monsters ? this.game.monsters.find(m => m.id === id) : null; + + if (hero) { + this.selectedHero = hero; + this.updateHeroStats(hero); + this.showHeroCard(hero); + this.hideMonsterCard(); // Hide monster card if showing + } else if (monster && this.selectedHero && this.game.turnManager.currentPhase === 'hero') { + // Show monster card only if a hero is selected (for attacking) + this.showMonsterCard(monster); + } } else { - this.selectedHero = null; - this.updateHeroStats(null); + // Deselection - check what type was deselected + if (this.selectedHero && this.selectedHero.id === id) { + // Hero was deselected + this.selectedHero = null; + this.updateHeroStats(null); + this.hideHeroCard(); + } else { + // Monster was deselected + this.hideMonsterCard(); + } } }; @@ -37,6 +55,10 @@ export class UIManager { this.game.onEntityMove = (entity, path) => { if (originalMove) originalMove(entity, path); this.updateHeroStats(entity); + // Update hero card if it's a hero + if (entity.type === 'hero') { + this.updateHeroCard(entity.id); + } }; } @@ -303,6 +325,412 @@ export class UIManager { placementControls.appendChild(this.discardBtn); } + createHeroCardsPanel() { + // Container for character cards (left side) + this.cardsContainer = document.createElement('div'); + this.cardsContainer.style.position = 'absolute'; + this.cardsContainer.style.left = '10px'; + this.cardsContainer.style.top = '220px'; // Below minimap + this.cardsContainer.style.display = 'flex'; + this.cardsContainer.style.flexDirection = 'column'; + this.cardsContainer.style.gap = '10px'; + this.cardsContainer.style.pointerEvents = 'auto'; + this.cardsContainer.style.width = '200px'; + this.container.appendChild(this.cardsContainer); + + // Create placeholder card + this.createPlaceholderCard(); + + // Store references + this.currentHeroCard = null; + this.currentMonsterCard = null; + this.attackButton = null; + } + + createPlaceholderCard() { + const card = document.createElement('div'); + card.style.width = '180px'; + card.style.height = '280px'; + card.style.backgroundColor = 'rgba(20, 20, 20, 0.95)'; + card.style.border = '2px solid #8B4513'; + card.style.borderRadius = '8px'; + card.style.padding = '10px'; + card.style.fontFamily = '"Cinzel", serif'; + card.style.color = '#888'; + card.style.display = 'flex'; + card.style.flexDirection = 'column'; + card.style.alignItems = 'center'; + card.style.justifyContent = 'center'; + card.style.textAlign = 'center'; + + // Circular icon container + const iconContainer = document.createElement('div'); + iconContainer.style.width = '100px'; + iconContainer.style.height = '100px'; + iconContainer.style.borderRadius = '50%'; + iconContainer.style.border = '2px solid #8B4513'; + iconContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; + iconContainer.style.display = 'flex'; + iconContainer.style.alignItems = 'center'; + iconContainer.style.justifyContent = 'center'; + iconContainer.style.marginBottom = '20px'; + + const icon = document.createElement('div'); + icon.textContent = '🎴'; + icon.style.fontSize = '48px'; + iconContainer.appendChild(icon); + card.appendChild(iconContainer); + + const text = document.createElement('div'); + text.textContent = 'Selecciona un Aventurero'; + text.style.fontSize = '14px'; + text.style.color = '#DAA520'; + card.appendChild(text); + + this.placeholderCard = card; + this.cardsContainer.appendChild(card); + } + + createHeroCard(hero) { + const card = document.createElement('div'); + card.style.width = '180px'; + card.style.backgroundColor = 'rgba(20, 20, 20, 0.95)'; + card.style.border = '2px solid #8B4513'; + card.style.borderRadius = '8px'; + card.style.padding = '10px'; + card.style.fontFamily = '"Cinzel", serif'; + card.style.color = '#fff'; + card.style.transition = 'all 0.3s'; + card.style.cursor = 'pointer'; + + // Hover effect + card.onmouseenter = () => { + card.style.borderColor = '#DAA520'; + card.style.transform = 'scale(1.05)'; + }; + card.onmouseleave = () => { + card.style.borderColor = '#8B4513'; + card.style.transform = 'scale(1)'; + }; + + // Click to select hero + card.onclick = () => { + if (this.game.onCellClick) { + this.game.onCellClick(hero.x, hero.y); + } + }; + + // Portrait (circular) + const portrait = document.createElement('div'); + portrait.style.width = '100px'; + portrait.style.height = '100px'; + portrait.style.borderRadius = '50%'; + portrait.style.overflow = 'hidden'; + portrait.style.border = '2px solid #DAA520'; + portrait.style.marginBottom = '8px'; + portrait.style.marginLeft = 'auto'; + portrait.style.marginRight = 'auto'; + portrait.style.backgroundColor = '#000'; + portrait.style.display = 'flex'; + portrait.style.alignItems = 'center'; + portrait.style.justifyContent = 'center'; + + // Use token image (placeholder for now) + const tokenPath = `/assets/images/dungeon1/tokens/heroes/${hero.key}.png?v=2`; + const img = document.createElement('img'); + img.src = tokenPath; + img.style.width = '100%'; + img.style.height = '100%'; + img.style.objectFit = 'cover'; + + // Fallback if image doesn't exist + img.onerror = () => { + portrait.innerHTML = `
?
`; + }; + + portrait.appendChild(img); + card.appendChild(portrait); + + // Name + const name = document.createElement('div'); + name.textContent = hero.name; + name.style.fontSize = '16px'; + name.style.fontWeight = 'bold'; + name.style.color = '#DAA520'; + name.style.textAlign = 'center'; + name.style.marginBottom = '8px'; + name.style.textTransform = 'uppercase'; + card.appendChild(name); + + // Lantern indicator + if (hero.hasLantern) { + const lantern = document.createElement('div'); + lantern.textContent = '🏮 Portador de la Lámpara'; + lantern.style.fontSize = '10px'; + lantern.style.color = '#FFA500'; + lantern.style.textAlign = 'center'; + lantern.style.marginBottom = '8px'; + card.appendChild(lantern); + } + + // Stats grid + const statsGrid = document.createElement('div'); + statsGrid.style.display = 'grid'; + statsGrid.style.gridTemplateColumns = '1fr 1fr'; + statsGrid.style.gap = '4px'; + statsGrid.style.fontSize = '12px'; + statsGrid.style.marginBottom = '8px'; + + const stats = [ + { label: 'WS', value: hero.stats.ws || 0 }, + { label: 'BS', value: hero.stats.bs || 0 }, + { label: 'S', value: hero.stats.str || 0 }, + { label: 'T', value: hero.stats.toughness || 0 }, + { label: 'W', value: `${hero.currentWounds || hero.stats.wounds}/${hero.stats.wounds}` }, + { label: 'I', value: hero.stats.initiative || 0 }, + { label: 'A', value: hero.stats.attacks || 0 }, + { label: 'Mov', value: `${hero.currentMoves || 0}/${hero.stats.move}` } + ]; + + stats.forEach(stat => { + const statEl = document.createElement('div'); + statEl.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; + statEl.style.padding = '3px 5px'; + statEl.style.borderRadius = '3px'; + statEl.style.display = 'flex'; + statEl.style.justifyContent = 'space-between'; + + const label = document.createElement('span'); + label.textContent = stat.label + ':'; + label.style.color = '#AAA'; + + const value = document.createElement('span'); + value.textContent = stat.value; + value.style.color = '#FFF'; + value.style.fontWeight = 'bold'; + + statEl.appendChild(label); + statEl.appendChild(value); + statsGrid.appendChild(statEl); + }); + + card.appendChild(statsGrid); + card.dataset.heroId = hero.id; + + return card; + } + + showHeroCard(hero) { + // Remove placeholder if present + if (this.placeholderCard && this.placeholderCard.parentNode) { + this.cardsContainer.removeChild(this.placeholderCard); + } + + // Remove previous hero card if present + if (this.currentHeroCard && this.currentHeroCard.parentNode) { + this.cardsContainer.removeChild(this.currentHeroCard); + } + + // Create and show new hero card + this.currentHeroCard = this.createHeroCard(hero); + this.cardsContainer.insertBefore(this.currentHeroCard, this.cardsContainer.firstChild); + } + + hideHeroCard() { + // Remove hero card + if (this.currentHeroCard && this.currentHeroCard.parentNode) { + this.cardsContainer.removeChild(this.currentHeroCard); + this.currentHeroCard = null; + } + + // Show placeholder if no cards are visible + if (!this.currentMonsterCard && this.placeholderCard && !this.placeholderCard.parentNode) { + this.cardsContainer.appendChild(this.placeholderCard); + } + } + + updateHeroCard(heroId) { + if (!this.currentHeroCard || this.currentHeroCard.dataset.heroId !== heroId) { + return; + } + + const hero = this.game.heroes.find(h => h.id === heroId); + if (!hero) return; + + // Update wounds and moves in the stats grid + const statsGrid = this.currentHeroCard.querySelector('div[style*="grid-template-columns"]'); + if (statsGrid) { + const statDivs = statsGrid.children; + // W is at index 4, Mov is at index 7 + if (statDivs[4]) { + const wValue = statDivs[4].querySelector('span:last-child'); + if (wValue) wValue.textContent = `${hero.currentWounds || hero.stats.wounds}/${hero.stats.wounds}`; + } + if (statDivs[7]) { + const movValue = statDivs[7].querySelector('span:last-child'); + if (movValue) movValue.textContent = `${hero.currentMoves || 0}/${hero.stats.move}`; + } + } + } + + createMonsterCard(monster) { + const card = document.createElement('div'); + card.style.width = '180px'; + card.style.backgroundColor = 'rgba(40, 20, 20, 0.95)'; + card.style.border = '2px solid #8B0000'; + card.style.borderRadius = '8px'; + card.style.padding = '10px'; + card.style.fontFamily = '"Cinzel", serif'; + card.style.color = '#fff'; + + const portrait = document.createElement('div'); + portrait.style.width = '100px'; + portrait.style.height = '100px'; + portrait.style.borderRadius = '50%'; + portrait.style.overflow = 'hidden'; + portrait.style.border = '2px solid #8B0000'; + portrait.style.marginBottom = '8px'; + portrait.style.marginLeft = 'auto'; + portrait.style.marginRight = 'auto'; + portrait.style.backgroundColor = '#000'; + portrait.style.display = 'flex'; + portrait.style.alignItems = 'center'; + portrait.style.justifyContent = 'center'; + + const tokenPath = `/assets/images/dungeon1/tokens/enemies/${monster.key}.png?v=2`; + const img = document.createElement('img'); + img.src = tokenPath; + img.style.width = '100%'; + img.style.height = '100%'; + img.style.objectFit = 'cover'; + + img.onerror = () => { + portrait.innerHTML = `
👹
`; + }; + + portrait.appendChild(img); + card.appendChild(portrait); + + const name = document.createElement('div'); + name.textContent = monster.name; + name.style.fontSize = '16px'; + name.style.fontWeight = 'bold'; + name.style.color = '#FF4444'; + name.style.textAlign = 'center'; + name.style.marginBottom = '8px'; + name.style.textTransform = 'uppercase'; + card.appendChild(name); + + const statsGrid = document.createElement('div'); + statsGrid.style.display = 'grid'; + statsGrid.style.gridTemplateColumns = '1fr 1fr'; + statsGrid.style.gap = '4px'; + statsGrid.style.fontSize = '12px'; + + const stats = [ + { label: 'WS', value: monster.stats.ws || 0 }, + { label: 'S', value: monster.stats.str || 0 }, + { label: 'T', value: monster.stats.toughness || 0 }, + { label: 'W', value: `${monster.currentWounds || monster.stats.wounds}/${monster.stats.wounds}` }, + { label: 'I', value: monster.stats.initiative || 0 }, + { label: 'A', value: monster.stats.attacks || 0 } + ]; + + stats.forEach(stat => { + const statEl = document.createElement('div'); + statEl.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; + statEl.style.padding = '3px 5px'; + statEl.style.borderRadius = '3px'; + statEl.style.display = 'flex'; + statEl.style.justifyContent = 'space-between'; + + const label = document.createElement('span'); + label.textContent = stat.label + ':'; + label.style.color = '#AAA'; + + const value = document.createElement('span'); + value.textContent = stat.value; + value.style.color = '#FFF'; + value.style.fontWeight = 'bold'; + + statEl.appendChild(label); + statEl.appendChild(value); + statsGrid.appendChild(statEl); + }); + + card.appendChild(statsGrid); + card.dataset.monsterId = monster.id; + + return card; + } + + showMonsterCard(monster) { + if (this.currentMonsterCard && this.currentMonsterCard.parentNode) { + this.cardsContainer.removeChild(this.currentMonsterCard); + } + + if (this.attackButton && this.attackButton.parentNode) { + this.cardsContainer.removeChild(this.attackButton); + } + + this.currentMonsterCard = this.createMonsterCard(monster); + this.cardsContainer.appendChild(this.currentMonsterCard); + + this.attackButton = document.createElement('button'); + this.attackButton.textContent = '⚔️ ATACAR'; + this.attackButton.style.width = '180px'; + this.attackButton.style.padding = '12px'; + this.attackButton.style.backgroundColor = '#8B0000'; + this.attackButton.style.color = '#fff'; + this.attackButton.style.border = '2px solid #FF4444'; + this.attackButton.style.borderRadius = '8px'; + this.attackButton.style.fontFamily = '"Cinzel", serif'; + this.attackButton.style.fontSize = '16px'; + this.attackButton.style.fontWeight = 'bold'; + this.attackButton.style.cursor = 'pointer'; + this.attackButton.style.transition = 'all 0.2s'; + + this.attackButton.onmouseenter = () => { + this.attackButton.style.backgroundColor = '#FF0000'; + this.attackButton.style.transform = 'scale(1.05)'; + }; + this.attackButton.onmouseleave = () => { + this.attackButton.style.backgroundColor = '#8B0000'; + this.attackButton.style.transform = 'scale(1)'; + }; + + this.attackButton.onclick = () => { + if (this.game.performHeroAttack) { + const result = this.game.performHeroAttack(monster.id); + if (result && result.success) { + // Attack successful, hide monster card + this.hideMonsterCard(); + // Deselect monster + if (this.game.selectedMonster) { + if (this.game.onEntitySelect) { + this.game.onEntitySelect(this.game.selectedMonster.id, false); + } + this.game.selectedMonster = null; + } + } + } + }; + + this.cardsContainer.appendChild(this.attackButton); + } + + hideMonsterCard() { + if (this.currentMonsterCard && this.currentMonsterCard.parentNode) { + this.cardsContainer.removeChild(this.currentMonsterCard); + this.currentMonsterCard = null; + } + + if (this.attackButton && this.attackButton.parentNode) { + this.cardsContainer.removeChild(this.attackButton); + this.attackButton = null; + } + } + showPlacementControls(show) { if (this.placementPanel) { this.placementPanel.style.display = show ? 'block' : 'none'; @@ -479,6 +907,13 @@ export class UIManager { this.notificationArea.style.opacity = '1'; + // Update hero card if defender is a hero + const defender = this.game.heroes.find(h => h.id === log.defenderId) || + this.game.monsters.find(m => m.id === log.defenderId); + if (defender && defender.type === 'hero') { + this.updateHeroCard(defender.id); + } + setTimeout(() => { if (this.notificationArea) this.notificationArea.style.opacity = '0'; }, 3500); diff --git a/src/view/UIManager.js.backup b/src/view/UIManager.js.backup new file mode 100644 index 0000000..83f1656 --- /dev/null +++ b/src/view/UIManager.js.backup @@ -0,0 +1,965 @@ +import { DIRECTIONS } from '../engine/dungeon/Constants.js'; + +export class UIManager { + constructor(cameraManager, gameEngine) { + this.cameraManager = cameraManager; + this.game = gameEngine; + this.dungeon = gameEngine.dungeon; + this.selectedHero = null; + + this.createHUD(); + this.createHeroCardsPanel(); // NEW: Hero stat cards + this.createGameStatusPanel(); // New Panel + this.setupMinimapLoop(); + this.setupGameListeners(); // New Listeners + + // Hook into engine callbacks for UI updates + const originalSelect = this.game.onEntitySelect; + this.game.onEntitySelect = (id, isSelected) => { + // 1. Call Renderer (was in main.js) + if (this.cameraManager && this.cameraManager.renderer) { + this.cameraManager.renderer.toggleEntitySelection(id, isSelected); + } else if (window.RENDERER) { + window.RENDERER.toggleEntitySelection(id, isSelected); + } + + // 2. Update UI + if (isSelected) { + const hero = this.game.heroes.find(h => h.id === id); + this.selectedHero = hero; // Store state + this.updateHeroStats(hero); + } else { + this.selectedHero = null; + this.updateHeroStats(null); + } + }; + + const originalMove = this.game.onEntityMove; + this.game.onEntityMove = (entity, path) => { + if (originalMove) originalMove(entity, path); + this.updateHeroStats(entity); + // Update hero card if it's a hero + if (entity.type === 'hero') { + this.updateHeroCard(entity.id); + } + }; + } + + createHUD() { + // Container + this.container = document.createElement('div'); + this.container.style.position = 'absolute'; + this.container.style.top = '0'; + this.container.style.left = '0'; + this.container.style.width = '100%'; + this.container.style.height = '100%'; + this.container.style.pointerEvents = 'none'; // Click through to 3D scene + document.body.appendChild(this.container); + + // --- Minimap (Top Left) --- + this.minimapCanvas = document.createElement('canvas'); + this.minimapCanvas.width = 200; + this.minimapCanvas.height = 200; + this.minimapCanvas.style.position = 'absolute'; + this.minimapCanvas.style.top = '10px'; + this.minimapCanvas.style.left = '10px'; + this.minimapCanvas.style.border = '2px solid #444'; + this.minimapCanvas.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'; + this.minimapCanvas.style.pointerEvents = 'auto'; // Allow interaction if needed + this.container.appendChild(this.minimapCanvas); + + this.ctx = this.minimapCanvas.getContext('2d'); + + // --- Camera Controls (Top Right) --- + const controlsContainer = document.createElement('div'); + controlsContainer.style.position = 'absolute'; + controlsContainer.style.top = '20px'; + controlsContainer.style.right = '20px'; + controlsContainer.style.display = 'flex'; + controlsContainer.style.gap = '10px'; + controlsContainer.style.alignItems = 'center'; + controlsContainer.style.pointerEvents = 'auto'; + this.container.appendChild(controlsContainer); + + // Zoom slider (vertical) + const zoomContainer = document.createElement('div'); + zoomContainer.style.display = 'flex'; + zoomContainer.style.flexDirection = 'column'; + zoomContainer.style.alignItems = 'center'; + zoomContainer.style.gap = '0px'; + zoomContainer.style.height = '140px'; // Fixed height to accommodate label + slider + + // Zoom label + const zoomLabel = document.createElement('div'); + zoomLabel.textContent = 'Zoom'; + zoomLabel.style.color = '#fff'; + zoomLabel.style.fontSize = '15px'; + zoomLabel.style.fontFamily = 'sans-serif'; + zoomLabel.style.marginBottom = '10px'; + zoomLabel.style.marginTop = '0px'; + + const zoomSlider = document.createElement('input'); + zoomSlider.type = 'range'; + zoomSlider.min = '3'; + zoomSlider.max = '15'; + zoomSlider.value = '6'; + zoomSlider.step = '0.5'; + zoomSlider.style.width = '100px'; + zoomSlider.style.transform = 'rotate(-90deg)'; + zoomSlider.style.transformOrigin = 'center'; + zoomSlider.style.cursor = 'pointer'; + zoomSlider.style.marginTop = '40px'; + + this.zoomSlider = zoomSlider; + + // Set initial zoom + this.cameraManager.zoomLevel = 6; + this.cameraManager.updateProjection(); + + this.cameraManager.onZoomChange = (val) => { + if (this.zoomSlider) this.zoomSlider.value = val; + }; + + zoomSlider.oninput = (e) => { + this.cameraManager.zoomLevel = parseFloat(e.target.value); + this.cameraManager.updateProjection(); + }; + + zoomContainer.appendChild(zoomLabel); + zoomContainer.appendChild(zoomSlider); + + // Direction buttons grid + const buttonsGrid = document.createElement('div'); + buttonsGrid.style.display = 'grid'; + buttonsGrid.style.gridTemplateColumns = '40px 40px 40px'; + buttonsGrid.style.gap = '5px'; + + controlsContainer.appendChild(zoomContainer); + controlsContainer.appendChild(buttonsGrid); + + const createBtn = (label, dir) => { + const btn = document.createElement('button'); + btn.textContent = label; + btn.style.width = '40px'; + btn.style.height = '40px'; + btn.style.backgroundColor = '#333'; + btn.style.color = '#fff'; + btn.style.border = '1px solid #666'; + btn.style.cursor = 'pointer'; + btn.style.transition = 'background-color 0.2s'; + btn.dataset.direction = dir; // Store direction for later reference + btn.onclick = () => { + this.cameraManager.setIsoView(dir); + this.updateActiveViewButton(dir); + }; + return btn; + }; + + // Layout: [N] + // [W] [E] + // [S] + + // Grid cells: 1 2 3 + const btnN = createBtn('N', DIRECTIONS.NORTH); btnN.style.gridColumn = '2'; + const btnW = createBtn('W', DIRECTIONS.WEST); btnW.style.gridColumn = '1'; + const btnE = createBtn('E', DIRECTIONS.EAST); btnE.style.gridColumn = '3'; + const btnS = createBtn('S', DIRECTIONS.SOUTH); btnS.style.gridColumn = '2'; + + buttonsGrid.appendChild(btnN); + buttonsGrid.appendChild(btnW); + buttonsGrid.appendChild(btnE); + buttonsGrid.appendChild(btnS); + + // Store button references for later updates + this.viewButtons = [btnN, btnE, btnS, btnW]; + + // Set initial active button (North) + this.updateActiveViewButton(DIRECTIONS.NORTH); + + // --- Tile Placement Controls (Bottom Center) --- + this.placementPanel = document.createElement('div'); + this.placementPanel.style.position = 'absolute'; + this.placementPanel.style.bottom = '20px'; + this.placementPanel.style.left = '50%'; + this.placementPanel.style.transform = 'translateX(-50%)'; + this.placementPanel.style.display = 'none'; // Hidden by default + this.placementPanel.style.pointerEvents = 'auto'; + this.placementPanel.style.backgroundColor = 'rgba(0, 0, 0, 0.85)'; + this.placementPanel.style.padding = '15px'; + this.placementPanel.style.borderRadius = '8px'; + this.placementPanel.style.border = '2px solid #666'; + this.container.appendChild(this.placementPanel); + + // Status text + this.placementStatus = document.createElement('div'); + this.placementStatus.style.color = '#fff'; + this.placementStatus.style.fontSize = '16px'; + this.placementStatus.style.fontFamily = 'sans-serif'; + this.placementStatus.style.marginBottom = '10px'; + this.placementStatus.style.textAlign = 'center'; + this.placementStatus.textContent = 'Coloca la loseta'; + this.placementPanel.appendChild(this.placementStatus); + + // Controls container + const placementControls = document.createElement('div'); + placementControls.style.display = 'flex'; + placementControls.style.gap = '15px'; + placementControls.style.alignItems = 'center'; + this.placementPanel.appendChild(placementControls); + + // Movement arrows (4-way grid) + const arrowGrid = document.createElement('div'); + arrowGrid.style.display = 'grid'; + arrowGrid.style.gridTemplateColumns = '40px 40px 40px'; + arrowGrid.style.gap = '3px'; + + const createArrow = (label, dx, dy) => { + const btn = document.createElement('button'); + btn.textContent = label; + btn.style.width = '40px'; + btn.style.height = '40px'; + btn.style.backgroundColor = '#444'; + btn.style.color = '#fff'; + btn.style.border = '1px solid #888'; + btn.style.cursor = 'pointer'; + btn.style.fontSize = '18px'; + btn.onclick = () => { + if (this.dungeon) { + this.dungeon.movePlacement(dx, dy); + } + }; + return btn; + }; + + const arrowUp = createArrow('↑', 0, 1); + const arrowLeft = createArrow('←', -1, 0); + const arrowRight = createArrow('→', 1, 0); + const arrowDown = createArrow('↓', 0, -1); + + arrowUp.style.gridColumn = '2'; + arrowLeft.style.gridColumn = '1'; + arrowRight.style.gridColumn = '3'; + arrowDown.style.gridColumn = '2'; + + arrowGrid.appendChild(arrowUp); + arrowGrid.appendChild(arrowLeft); + arrowGrid.appendChild(arrowRight); + arrowGrid.appendChild(arrowDown); + + placementControls.appendChild(arrowGrid); + + // Rotate button + this.rotateBtn = document.createElement('button'); + this.rotateBtn.textContent = '🔄 Rotar'; + this.rotateBtn.style.padding = '10px 20px'; + this.rotateBtn.style.backgroundColor = '#555'; + this.rotateBtn.style.color = '#fff'; + this.rotateBtn.style.border = '1px solid #888'; + this.rotateBtn.style.cursor = 'pointer'; + this.rotateBtn.style.fontSize = '16px'; + this.rotateBtn.style.borderRadius = '4px'; + this.rotateBtn.onclick = () => { + if (this.dungeon) { + this.dungeon.rotatePlacement(); + } + }; + placementControls.appendChild(this.rotateBtn); + + this.placeBtn = document.createElement('button'); + this.placeBtn.textContent = '⬇ Bajar'; + this.placeBtn.style.padding = '10px 20px'; + this.placeBtn.style.backgroundColor = '#2a5'; + this.placeBtn.style.color = '#fff'; + this.placeBtn.style.border = '1px solid #888'; + this.placeBtn.style.cursor = 'pointer'; + this.placeBtn.style.fontSize = '16px'; + this.placeBtn.style.borderRadius = '4px'; + this.placeBtn.onclick = () => { + if (this.dungeon) { + const success = this.dungeon.confirmPlacement(); + if (!success) { + this.showModal('Error de Colocación', 'No se puede colocar la loseta en esta posición.'); + } + } + }; + placementControls.appendChild(this.placeBtn); + + // Discard button + this.discardBtn = document.createElement('button'); + this.discardBtn.textContent = '❌ Cancelar'; + this.discardBtn.style.padding = '10px 20px'; + this.discardBtn.style.backgroundColor = '#d33'; + this.discardBtn.style.color = '#fff'; + this.discardBtn.style.border = '1px solid #888'; + this.discardBtn.style.cursor = 'pointer'; + this.discardBtn.style.fontSize = '16px'; + this.discardBtn.style.borderRadius = '4px'; + this.discardBtn.onclick = () => { + if (this.dungeon) { + this.showConfirm( + 'Confirmar acción', + '¿Quieres descartar esta loseta y bloquear la puerta?', + () => { + this.dungeon.cancelPlacement(); + } + ); + } + }; + placementControls.appendChild(this.discardBtn); + } + + createHeroCardsPanel() { + // Container for character cards (left side) + this.cardsContainer = document.createElement('div'); + this.cardsContainer.style.position = 'absolute'; + this.cardsContainer.style.left = '10px'; + this.cardsContainer.style.top = '220px'; // Below minimap + this.cardsContainer.style.display = 'flex'; + this.cardsContainer.style.flexDirection = 'column'; + this.cardsContainer.style.gap = '10px'; + this.cardsContainer.style.pointerEvents = 'auto'; + this.cardsContainer.style.width = '200px'; + this.container.appendChild(this.cardsContainer); + + // Create placeholder card + this.createPlaceholderCard(); + + // Store references + this.currentHeroCard = null; + this.currentMonsterCard = null; + this.attackButton = null; + } + + createPlaceholderCard() { + const card = document.createElement('div'); + card.style.width = '180px'; + card.style.height = '280px'; + card.style.backgroundColor = 'rgba(20, 20, 20, 0.95)'; + card.style.border = '2px solid #8B4513'; + card.style.borderRadius = '8px'; + card.style.padding = '10px'; + card.style.fontFamily = '"Cinzel", serif'; + card.style.color = '#888'; + card.style.display = 'flex'; + card.style.flexDirection = 'column'; + card.style.alignItems = 'center'; + card.style.justifyContent = 'center'; + card.style.textAlign = 'center'; + + const icon = document.createElement('div'); + icon.textContent = '🎴'; + icon.style.fontSize = '64px'; + icon.style.marginBottom = '20px'; + card.appendChild(icon); + + const text = document.createElement('div'); + text.textContent = 'Selecciona un Aventurero'; + text.style.fontSize = '14px'; + text.style.color = '#DAA520'; + card.appendChild(text); + + this.placeholderCard = card; + this.cardsContainer.appendChild(card); + } + + createHeroCard(hero) { + const card = document.createElement('div'); + card.style.width = '180px'; + card.style.backgroundColor = 'rgba(20, 20, 20, 0.95)'; + card.style.border = '2px solid #8B4513'; + card.style.borderRadius = '8px'; + card.style.padding = '10px'; + card.style.fontFamily = '"Cinzel", serif'; + card.style.color = '#fff'; + card.style.transition = 'all 0.3s'; + card.style.cursor = 'pointer'; + + // Hover effect + card.onmouseenter = () => { + card.style.borderColor = '#DAA520'; + card.style.transform = 'scale(1.05)'; + }; + card.onmouseleave = () => { + card.style.borderColor = '#8B4513'; + card.style.transform = 'scale(1)'; + }; + + // Click to select hero + card.onclick = () => { + if (this.game.onCellClick) { + this.game.onCellClick(hero.x, hero.y); + } + }; + + // Portrait + const portrait = document.createElement('div'); + portrait.style.width = '100%'; + portrait.style.height = '100px'; + portrait.style.borderRadius = '5px'; + portrait.style.overflow = 'hidden'; + portrait.style.border = '2px solid #DAA520'; + portrait.style.marginBottom = '8px'; + portrait.style.backgroundColor = '#000'; + portrait.style.display = 'flex'; + portrait.style.alignItems = 'center'; + portrait.style.justifyContent = 'center'; + + // Use token image (placeholder for now) + const tokenPath = `/assets/images/dungeon1/tokens/heroes/${hero.key}.png`; + const img = document.createElement('img'); + img.src = tokenPath; + img.style.width = '100%'; + img.style.height = '100%'; + img.style.objectFit = 'cover'; + + // Fallback if image doesn't exist + img.onerror = () => { + portrait.innerHTML = `
?
`; + }; + + portrait.appendChild(img); + card.appendChild(portrait); + + // Name + const name = document.createElement('div'); + name.textContent = hero.name; + name.style.fontSize = '16px'; + name.style.fontWeight = 'bold'; + name.style.color = '#DAA520'; + name.style.textAlign = 'center'; + name.style.marginBottom = '8px'; + name.style.textTransform = 'uppercase'; + card.appendChild(name); + + // Lantern indicator + if (hero.hasLantern) { + const lantern = document.createElement('div'); + lantern.textContent = '🏮 Portador de la Lámpara'; + lantern.style.fontSize = '10px'; + lantern.style.color = '#FFA500'; + lantern.style.textAlign = 'center'; + lantern.style.marginBottom = '8px'; + card.appendChild(lantern); + } + + // Stats grid + const statsGrid = document.createElement('div'); + statsGrid.style.display = 'grid'; + statsGrid.style.gridTemplateColumns = '1fr 1fr'; + statsGrid.style.gap = '4px'; + statsGrid.style.fontSize = '12px'; + statsGrid.style.marginBottom = '8px'; + + const stats = [ + { label: 'WS', value: hero.stats.ws || 0 }, + { label: 'BS', value: hero.stats.bs || 0 }, + { label: 'S', value: hero.stats.str || 0 }, + { label: 'T', value: hero.stats.toughness || 0 }, + { label: 'W', value: `${hero.currentWounds || hero.stats.wounds}/${hero.stats.wounds}` }, + { label: 'I', value: hero.stats.initiative || 0 }, + { label: 'A', value: hero.stats.attacks || 0 }, + { label: 'Mov', value: `${hero.currentMoves || 0}/${hero.stats.move}` } + ]; + + stats.forEach(stat => { + const statEl = document.createElement('div'); + statEl.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; + statEl.style.padding = '3px 5px'; + statEl.style.borderRadius = '3px'; + statEl.style.display = 'flex'; + statEl.style.justifyContent = 'space-between'; + + const label = document.createElement('span'); + label.textContent = stat.label + ':'; + label.style.color = '#AAA'; + + const value = document.createElement('span'); + value.textContent = stat.value; + value.style.color = '#FFF'; + value.style.fontWeight = 'bold'; + + statEl.appendChild(label); + statEl.appendChild(value); + statsGrid.appendChild(statEl); + }); + + card.appendChild(statsGrid); + + // Store reference + this.heroCards.set(hero.id, card); + this.heroCardsContainer.appendChild(card); + } + + updateHeroCard(heroId) { + const card = this.heroCards.get(heroId); + if (!card) return; + + const hero = this.game.heroes.find(h => h.id === heroId); + if (!hero) return; + + // Update wounds and moves in the stats grid + const statsGrid = card.querySelector('div[style*="grid-template-columns"]'); + if (statsGrid) { + const statDivs = statsGrid.children; + // W is at index 4, Mov is at index 7 + if (statDivs[4]) { + const wValue = statDivs[4].querySelector('span:last-child'); + if (wValue) wValue.textContent = `${hero.currentWounds || hero.stats.wounds}/${hero.stats.wounds}`; + } + if (statDivs[7]) { + const movValue = statDivs[7].querySelector('span:last-child'); + if (movValue) movValue.textContent = `${hero.currentMoves || 0}/${hero.stats.move}`; + } + } + } + + showPlacementControls(show) { + if (this.placementPanel) { + this.placementPanel.style.display = show ? 'block' : 'none'; + } + } + + updatePlacementStatus(isValid) { + if (this.placementStatus) { + if (isValid) { + this.placementStatus.textContent = '✅ Posición válida'; + this.placementStatus.style.color = '#0f0'; + this.placeBtn.style.backgroundColor = '#2a5'; + this.placeBtn.style.cursor = 'pointer'; + } else { + this.placementStatus.textContent = '❌ Posición inválida'; + this.placementStatus.style.color = '#f44'; + this.placeBtn.style.backgroundColor = '#555'; + this.placeBtn.style.cursor = 'not-allowed'; + } + } + } + + updateActiveViewButton(activeDirection) { + // Reset all buttons to default color + this.viewButtons.forEach(btn => { + btn.style.backgroundColor = '#333'; + }); + + // Highlight the active button + const activeBtn = this.viewButtons.find(btn => btn.dataset.direction === activeDirection); + if (activeBtn) { + activeBtn.style.backgroundColor = '#f0c040'; // Yellow/gold color + } + } + + setupMinimapLoop() { + const loop = () => { + this.drawMinimap(); + requestAnimationFrame(loop); + }; + loop(); + } + + drawMinimap() { + const ctx = this.ctx; + const w = this.minimapCanvas.width; + const h = this.minimapCanvas.height; + + ctx.clearRect(0, 0, w, h); + + const cellSize = 5; + const centerX = w / 2; + const centerY = h / 2; + + ctx.fillStyle = '#666'; // Generic floor + + for (const [key, tileId] of this.dungeon.grid.occupiedCells) { + const [x, y] = key.split(',').map(Number); + const cx = centerX + (x * cellSize); + const cy = centerY - (y * cellSize); + + if (tileId.includes('room')) ctx.fillStyle = '#55a'; + else ctx.fillStyle = '#aaa'; + + ctx.fillRect(cx, cy, cellSize, cellSize); + } + + // Draw Exits (Available) + ctx.fillStyle = '#0f0'; // Green dots for open exits + if (this.dungeon.availableExits) { + this.dungeon.availableExits.forEach(exit => { + const ex = centerX + (exit.x * cellSize); + const ey = centerY - (exit.y * cellSize); + ctx.fillRect(ex, ey, cellSize, cellSize); + }); + } + + // Draw Entry (0,0) cross + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(centerX - 5, centerY); + ctx.lineTo(centerX + 5, centerY); + ctx.moveTo(centerX, centerY - 5); + ctx.lineTo(centerX, centerY + 5); + ctx.stroke(); + } + + showModal(title, message, onClose) { + // Overlay + const overlay = document.createElement('div'); + overlay.style.position = 'absolute'; + overlay.style.top = '0'; + overlay.style.left = '0'; + overlay.style.width = '100%'; + overlay.style.height = '100%'; + overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; + overlay.style.display = 'flex'; + overlay.style.justifyContent = 'center'; + overlay.style.alignItems = 'center'; + overlay.style.pointerEvents = 'auto'; // Block clicks behind + overlay.style.zIndex = '1000'; + + // Content Box + const content = document.createElement('div'); + content.style.backgroundColor = '#222'; + content.style.border = '2px solid #888'; + content.style.borderRadius = '8px'; + content.style.padding = '20px'; + content.style.width = '300px'; + content.style.textAlign = 'center'; + content.style.color = '#fff'; + content.style.fontFamily = 'sans-serif'; + + // Title + const titleEl = document.createElement('h2'); + titleEl.textContent = title; + titleEl.style.marginTop = '0'; + titleEl.style.color = '#f44'; // Reddish for importance + content.appendChild(titleEl); + + // Message + const msgEl = document.createElement('p'); + msgEl.innerHTML = message; + msgEl.style.fontSize = '16px'; + msgEl.style.lineHeight = '1.5'; + content.appendChild(msgEl); + + // OK Button + const btn = document.createElement('button'); + btn.textContent = 'Entendido'; + btn.style.marginTop = '20px'; + btn.style.padding = '10px 20px'; + btn.style.fontSize = '16px'; + btn.style.cursor = 'pointer'; + btn.style.backgroundColor = '#444'; + btn.style.color = '#fff'; + btn.style.border = '1px solid #888'; + btn.onclick = () => { + this.container.removeChild(overlay); + if (onClose) onClose(); + }; + content.appendChild(btn); + + overlay.appendChild(content); + this.container.appendChild(overlay); + } + + showCombatLog(log) { + if (!this.notificationArea) return; + + const isHit = log.hitSuccess; + const color = isHit ? '#ff4444' : '#aaaaaa'; + const title = isHit ? 'GOLPE!' : 'FALLO'; + + let detailHtml = ''; + if (isHit) { + if (log.woundsCaused > 0) { + detailHtml = `
-${log.woundsCaused} HP
`; + } else { + detailHtml = `
Sin Heridas (Armadura)
`; + } + } else { + detailHtml = `
Esquivado / Fallado
`; + } + + // Show simplified but impactful message + this.notificationArea.innerHTML = ` +
+
${log.attackerId.split('_')[0]} ATACA
+ ${detailHtml} +
${log.message}
+
+ `; + + this.notificationArea.style.opacity = '1'; + + // Update hero card if defender is a hero + const defender = this.game.heroes.find(h => h.id === log.defenderId) || + this.game.monsters.find(m => m.id === log.defenderId); + if (defender && defender.type === 'hero') { + this.updateHeroCard(defender.id); + } + + setTimeout(() => { + if (this.notificationArea) this.notificationArea.style.opacity = '0'; + }, 3500); + } + + showConfirm(title, message, onConfirm) { + // Overlay + const overlay = document.createElement('div'); + overlay.style.position = 'absolute'; + overlay.style.top = '0'; + overlay.style.left = '0'; + overlay.style.width = '100%'; + overlay.style.height = '100%'; + overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; + overlay.style.display = 'flex'; + overlay.style.justifyContent = 'center'; + overlay.style.alignItems = 'center'; + overlay.style.pointerEvents = 'auto'; // Block clicks behind + overlay.style.zIndex = '1000'; + + // Content Box + const content = document.createElement('div'); + content.style.backgroundColor = '#222'; + content.style.border = '2px solid #888'; + content.style.borderRadius = '8px'; + content.style.padding = '20px'; + content.style.width = '300px'; + content.style.textAlign = 'center'; + content.style.color = '#fff'; + content.style.fontFamily = 'sans-serif'; + + // Title + const titleEl = document.createElement('h2'); + titleEl.textContent = title; + titleEl.style.marginTop = '0'; + titleEl.style.color = '#f44'; + content.appendChild(titleEl); + + // Message + const msgEl = document.createElement('p'); + msgEl.innerHTML = message; + msgEl.style.fontSize = '16px'; + msgEl.style.lineHeight = '1.5'; + content.appendChild(msgEl); + + // Buttons Container + const buttons = document.createElement('div'); + buttons.style.display = 'flex'; + buttons.style.justifyContent = 'space-around'; + buttons.style.marginTop = '20px'; + + // Cancel Button + const cancelBtn = document.createElement('button'); + cancelBtn.textContent = 'Cancelar'; + cancelBtn.style.padding = '10px 20px'; + cancelBtn.style.fontSize = '16px'; + cancelBtn.style.cursor = 'pointer'; + cancelBtn.style.backgroundColor = '#555'; + cancelBtn.style.color = '#fff'; + cancelBtn.style.border = '1px solid #888'; + cancelBtn.onclick = () => { + this.container.removeChild(overlay); + }; + buttons.appendChild(cancelBtn); + + // Confirm Button + const confirmBtn = document.createElement('button'); + confirmBtn.textContent = 'Aceptar'; + confirmBtn.style.padding = '10px 20px'; + confirmBtn.style.fontSize = '16px'; + confirmBtn.style.cursor = 'pointer'; + confirmBtn.style.backgroundColor = '#2a5'; + confirmBtn.style.color = '#fff'; + confirmBtn.style.border = '1px solid #888'; + confirmBtn.onclick = () => { + if (onConfirm) onConfirm(); + this.container.removeChild(overlay); + }; + buttons.appendChild(confirmBtn); + + content.appendChild(buttons); + overlay.appendChild(content); + this.container.appendChild(overlay); + } + + createGameStatusPanel() { + // Top Center Panel + this.statusPanel = document.createElement('div'); + this.statusPanel.style.position = 'absolute'; + this.statusPanel.style.top = '20px'; + this.statusPanel.style.left = '50%'; + this.statusPanel.style.transform = 'translateX(-50%)'; + this.statusPanel.style.display = 'flex'; + this.statusPanel.style.flexDirection = 'column'; + this.statusPanel.style.alignItems = 'center'; + this.statusPanel.style.pointerEvents = 'none'; + + // Turn/Phase Info + this.phaseInfo = document.createElement('div'); + this.phaseInfo.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'; + this.phaseInfo.style.padding = '10px 20px'; + this.phaseInfo.style.border = '2px solid #daa520'; // GoldenRod + this.phaseInfo.style.borderRadius = '5px'; + this.phaseInfo.style.color = '#fff'; + this.phaseInfo.style.fontFamily = '"Cinzel", serif'; + this.phaseInfo.style.fontSize = '20px'; + this.phaseInfo.style.textAlign = 'center'; + this.phaseInfo.style.textTransform = 'uppercase'; + this.phaseInfo.style.minWidth = '200px'; + this.phaseInfo.innerHTML = ` +
Turn 1
+
Setup
+ `; + + this.statusPanel.appendChild(this.phaseInfo); + + // End Phase Button + this.endPhaseBtn = document.createElement('button'); + this.endPhaseBtn.textContent = 'ACABAR FASE AVENTUREROS'; + this.endPhaseBtn.style.marginTop = '10px'; + this.endPhaseBtn.style.width = '100%'; + this.endPhaseBtn.style.padding = '8px'; + this.endPhaseBtn.style.backgroundColor = '#daa520'; // Gold + this.endPhaseBtn.style.color = '#000'; + this.endPhaseBtn.style.border = '1px solid #8B4513'; + this.endPhaseBtn.style.borderRadius = '3px'; + this.endPhaseBtn.style.fontWeight = 'bold'; + this.endPhaseBtn.style.cursor = 'pointer'; + this.endPhaseBtn.style.display = 'none'; // Hidden by default + this.endPhaseBtn.style.fontFamily = '"Cinzel", serif'; + this.endPhaseBtn.style.fontSize = '12px'; + this.endPhaseBtn.style.pointerEvents = 'auto'; // Enable clicking + + this.endPhaseBtn.onmouseover = () => { this.endPhaseBtn.style.backgroundColor = '#ffd700'; }; + this.endPhaseBtn.onmouseout = () => { this.endPhaseBtn.style.backgroundColor = '#daa520'; }; + + this.endPhaseBtn.onclick = () => { + console.log('[UIManager] End Phase Button Clicked', this.game.turnManager.currentPhase); + this.game.turnManager.nextPhase(); + }; + this.statusPanel.appendChild(this.endPhaseBtn); + + // Notification Area (Power Roll results, etc) + this.notificationArea = document.createElement('div'); + this.notificationArea.style.marginTop = '10px'; + this.notificationArea.style.transition = 'opacity 0.5s'; + this.notificationArea.style.opacity = '0'; + this.statusPanel.appendChild(this.notificationArea); + + this.container.appendChild(this.statusPanel); + + // Inject Font + if (!document.getElementById('game-font')) { + const link = document.createElement('link'); + link.id = 'game-font'; + link.href = 'https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&display=swap'; + link.rel = 'stylesheet'; + document.head.appendChild(link); + } + } + + setupGameListeners() { + if (this.game.turnManager) { + this.game.turnManager.on('phase_changed', (phase) => { + this.updatePhaseDisplay(phase); + }); + + this.game.turnManager.on('POWER_RESULT', (data) => { + this.showPowerRollResult(data); + }); + } + } + + updatePhaseDisplay(phase) { + if (!this.phaseInfo) return; + const turn = this.game.turnManager.currentTurn; + + let content = ` +
Turn ${turn}
+
${phase.replace('_', ' ')}
+ `; + + if (this.selectedHero) { + content += this.getHeroStatsHTML(this.selectedHero); + } + + this.phaseInfo.innerHTML = content; + + if (this.endPhaseBtn) { + if (phase === 'hero') { + this.endPhaseBtn.style.display = 'block'; + this.endPhaseBtn.textContent = 'ACABAR FASE AVENTUREROS'; + this.endPhaseBtn.title = "Pasar a la Fase de Monstruos"; + } else if (phase === 'monster') { + this.endPhaseBtn.style.display = 'block'; + this.endPhaseBtn.textContent = 'ACABAR FASE MONSTRUOS'; + this.endPhaseBtn.title = "Pasar a Fase de Exploración"; + } else if (phase === 'exploration') { + this.endPhaseBtn.style.display = 'block'; + this.endPhaseBtn.textContent = 'ACABAR TURNO'; + this.endPhaseBtn.title = "Finalizar turno y comenzar Fase de Poder"; + } else { + this.endPhaseBtn.style.display = 'none'; + } + } + } + + updateHeroStats(hero) { + if (!this.phaseInfo) return; + + const turn = this.game.turnManager.currentTurn; + const phase = this.game.turnManager.currentPhase; + + let content = ` +
Turn ${turn}
+
${phase.replace('_', ' ')}
+ `; + + if (hero) { + content += this.getHeroStatsHTML(hero); + } + + this.phaseInfo.innerHTML = content; + } + + getHeroStatsHTML(hero) { + const portraitUrl = hero.texturePath || ''; + + const lanternIcon = hero.hasLantern ? '🏮' : ''; + + return ` +
+
+ ${hero.name} +
+
+
+ ${hero.name} ${lanternIcon} +
+
+ Moves: ${hero.currentMoves} / ${hero.stats.move} +
+
+
+ `; + } + + showPowerRollResult(data) { + if (!this.notificationArea) return; + const { roll, message, eventTriggered } = data; + const color = eventTriggered ? '#ff4444' : '#44ff44'; + + this.notificationArea.innerHTML = ` +
+
Power Phase
+
${roll}
+
${message}
+
+ `; + + this.notificationArea.style.opacity = '1'; + + setTimeout(() => { + if (this.notificationArea) this.notificationArea.style.opacity = '0'; + }, 3000); + } +}