import { DIRECTIONS } from '../engine/dungeon/Constants.js'; export class UIManager { constructor(cameraManager, gameEngine) { this.cameraManager = cameraManager; this.game = gameEngine; this.dungeon = gameEngine.dungeon; this.createHUD(); this.setupMinimapLoop(); } 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 = '2.5'; // Closest zoom zoomSlider.max = '30'; // Farthest zoom zoomSlider.value = '2.5'; // Start at closest 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'; // Push slider down to make room for label // Set initial zoom to closest this.cameraManager.zoomLevel = 2.5; this.cameraManager.updateProjection(); 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) { alert('❌ 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); } 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); // Center the view on 0,0 or the average? // Let's rely on fixed scale for now const cellSize = 5; const centerX = w / 2; const centerY = h / 2; // Draw placed tiles // We can access this.dungeon.grid.occupiedCells for raw occupied spots // Or this.dungeon.placedTiles for structural info (type, color) ctx.fillStyle = '#666'; // Generic floor // Iterate over grid occupied cells // But grid is a Map, iterating keys is slow. // Better to iterate placedTiles which is an Array // Simpler approach: Iterate the Grid Map directly // It's a Map<"x,y", tileId> // Use an iterator for (const [key, tileId] of this.dungeon.grid.occupiedCells) { const [x, y] = key.split(',').map(Number); // Coordinate transformation to Canvas // Dungeon (0,0) -> Canvas (CenterX, CenterY) // Y in dungeon is Up/North. Y in Canvas is Down. // So CanvasY = CenterY - (DungeonY * size) const cx = centerX + (x * cellSize); const cy = centerY - (y * cellSize); // Color based on TileId type? 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) { // 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.textContent = 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); }; content.appendChild(btn); overlay.appendChild(content); this.container.appendChild(overlay); } 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.textContent = 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); } }