- Updated TileDefinitions.js: Added 4-way exits to corridor_straight and corridor_steps (N/S y=3,4; E/W x=3,4). - Updated DungeonGenerator.js: Added cancelPlacement() logic and onDoorBlocked callback. - Updated GameRenderer.js: Implemented blockDoor() to visualize blocked passages, and improved isPlayerAdjacentToDoor. - Updated UIManager.js: Added custom showModal/showConfirm and Discard button for tile placement. - Updated main.js: Handled blocked door clicks and hooked up UI events. - Updated GameEngine.js: Improved door adjacency checks. - Updated CameraManager.js: Preserved camera rotation on centerOn. - Added door1_blocked.png asset.
514 lines
19 KiB
JavaScript
514 lines
19 KiB
JavaScript
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);
|
|
}
|
|
}
|