diff --git a/public/assets/images/dungeon1/doors/door1_blocked.png b/public/assets/images/dungeon1/doors/door1_blocked.png new file mode 100644 index 0000000..5da90c6 Binary files /dev/null and b/public/assets/images/dungeon1/doors/door1_blocked.png differ diff --git a/src/engine/dungeon/DungeonGenerator.js b/src/engine/dungeon/DungeonGenerator.js index 50759a1..756c142 100644 --- a/src/engine/dungeon/DungeonGenerator.js +++ b/src/engine/dungeon/DungeonGenerator.js @@ -27,6 +27,7 @@ export class DungeonGenerator { // Callbacks for UI this.onStateChange = null; this.onPlacementUpdate = null; + this.onDoorBlocked = null; } startDungeon(missionConfig) { @@ -152,6 +153,30 @@ export class DungeonGenerator { return this.grid.canPlace(variant, this.placementX, this.placementY); } + cancelPlacement() { + if (this.state !== PLACEMENT_STATE.PLACING_TILE) return; + + // 1. Mark door as blocked visually + if (this.onDoorBlocked && this.selectedExit) { + this.onDoorBlocked(this.selectedExit); + } + + // 2. Remove the selected exit from available exits + if (this.selectedExit) { + this.availableExits = this.availableExits.filter(e => + !(e.x === this.selectedExit.x && e.y === this.selectedExit.y && e.direction === this.selectedExit.direction) + ); + } + + // 3. Reset state + this.currentCard = null; + this.selectedExit = null; + this.state = PLACEMENT_STATE.WAITING_DOOR; + + this.notifyPlacementUpdate(); + this.notifyStateChange(); + } + /** * Confirm and finalize tile placement */ diff --git a/src/engine/dungeon/TileDefinitions.js b/src/engine/dungeon/TileDefinitions.js index 9a9d0de..25659d6 100644 --- a/src/engine/dungeon/TileDefinitions.js +++ b/src/engine/dungeon/TileDefinitions.js @@ -16,7 +16,9 @@ export const TILES = { layout: [[1, 1], [1, 1], [1, 1], [1, 1], [1, 1], [1, 1]], exits: [ { x: 0, y: 0, direction: DIRECTIONS.SOUTH }, { x: 1, y: 0, direction: DIRECTIONS.SOUTH }, - { x: 0, y: 5, direction: DIRECTIONS.NORTH }, { x: 1, y: 5, direction: DIRECTIONS.NORTH } + { x: 0, y: 5, direction: DIRECTIONS.NORTH }, { x: 1, y: 5, direction: DIRECTIONS.NORTH }, + { x: 0, y: 3, direction: DIRECTIONS.WEST }, { x: 0, y: 4, direction: DIRECTIONS.WEST }, + { x: 1, y: 3, direction: DIRECTIONS.EAST }, { x: 1, y: 4, direction: DIRECTIONS.EAST } ] }, [DIRECTIONS.SOUTH]: { @@ -24,7 +26,9 @@ export const TILES = { layout: [[1, 1], [1, 1], [1, 1], [1, 1], [1, 1], [1, 1]], exits: [ { x: 0, y: 0, direction: DIRECTIONS.SOUTH }, { x: 1, y: 0, direction: DIRECTIONS.SOUTH }, - { x: 0, y: 5, direction: DIRECTIONS.NORTH }, { x: 1, y: 5, direction: DIRECTIONS.NORTH } + { x: 0, y: 5, direction: DIRECTIONS.NORTH }, { x: 1, y: 5, direction: DIRECTIONS.NORTH }, + { x: 0, y: 3, direction: DIRECTIONS.WEST }, { x: 0, y: 4, direction: DIRECTIONS.WEST }, + { x: 1, y: 3, direction: DIRECTIONS.EAST }, { x: 1, y: 4, direction: DIRECTIONS.EAST } ] }, [DIRECTIONS.EAST]: { @@ -32,7 +36,9 @@ export const TILES = { layout: [[1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1]], exits: [ { x: 0, y: 0, direction: DIRECTIONS.WEST }, { x: 0, y: 1, direction: DIRECTIONS.WEST }, - { x: 5, y: 0, direction: DIRECTIONS.EAST }, { x: 5, y: 1, direction: DIRECTIONS.EAST } + { x: 5, y: 0, direction: DIRECTIONS.EAST }, { x: 5, y: 1, direction: DIRECTIONS.EAST }, + { x: 3, y: 0, direction: DIRECTIONS.SOUTH }, { x: 4, y: 0, direction: DIRECTIONS.SOUTH }, + { x: 3, y: 1, direction: DIRECTIONS.NORTH }, { x: 4, y: 1, direction: DIRECTIONS.NORTH } ] }, [DIRECTIONS.WEST]: { @@ -40,7 +46,9 @@ export const TILES = { layout: [[1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1]], exits: [ { x: 0, y: 0, direction: DIRECTIONS.WEST }, { x: 0, y: 1, direction: DIRECTIONS.WEST }, - { x: 5, y: 0, direction: DIRECTIONS.EAST }, { x: 5, y: 1, direction: DIRECTIONS.EAST } + { x: 5, y: 0, direction: DIRECTIONS.EAST }, { x: 5, y: 1, direction: DIRECTIONS.EAST }, + { x: 3, y: 0, direction: DIRECTIONS.SOUTH }, { x: 4, y: 0, direction: DIRECTIONS.SOUTH }, + { x: 3, y: 1, direction: DIRECTIONS.NORTH }, { x: 4, y: 1, direction: DIRECTIONS.NORTH } ] } } @@ -60,7 +68,9 @@ export const TILES = { layout: [[1, 1], [1, 1], [1, 1], [1, 1], [1, 1], [1, 1]], exits: [ { x: 0, y: 0, direction: DIRECTIONS.SOUTH }, { x: 1, y: 0, direction: DIRECTIONS.SOUTH }, - { x: 0, y: 5, direction: DIRECTIONS.NORTH }, { x: 1, y: 5, direction: DIRECTIONS.NORTH } + { x: 0, y: 5, direction: DIRECTIONS.NORTH }, { x: 1, y: 5, direction: DIRECTIONS.NORTH }, + { x: 0, y: 3, direction: DIRECTIONS.WEST }, { x: 0, y: 4, direction: DIRECTIONS.WEST }, + { x: 1, y: 3, direction: DIRECTIONS.EAST }, { x: 1, y: 4, direction: DIRECTIONS.EAST } ] }, [DIRECTIONS.SOUTH]: { @@ -68,7 +78,9 @@ export const TILES = { layout: [[1, 1], [1, 1], [1, 1], [1, 1], [1, 1], [1, 1]], exits: [ { x: 0, y: 0, direction: DIRECTIONS.SOUTH }, { x: 1, y: 0, direction: DIRECTIONS.SOUTH }, - { x: 0, y: 5, direction: DIRECTIONS.NORTH }, { x: 1, y: 5, direction: DIRECTIONS.NORTH } + { x: 0, y: 5, direction: DIRECTIONS.NORTH }, { x: 1, y: 5, direction: DIRECTIONS.NORTH }, + { x: 0, y: 3, direction: DIRECTIONS.WEST }, { x: 0, y: 4, direction: DIRECTIONS.WEST }, + { x: 1, y: 3, direction: DIRECTIONS.EAST }, { x: 1, y: 4, direction: DIRECTIONS.EAST } ] }, [DIRECTIONS.EAST]: { @@ -76,7 +88,9 @@ export const TILES = { layout: [[1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1]], exits: [ { x: 0, y: 0, direction: DIRECTIONS.WEST }, { x: 0, y: 1, direction: DIRECTIONS.WEST }, - { x: 5, y: 0, direction: DIRECTIONS.EAST }, { x: 5, y: 1, direction: DIRECTIONS.EAST } + { x: 5, y: 0, direction: DIRECTIONS.EAST }, { x: 5, y: 1, direction: DIRECTIONS.EAST }, + { x: 3, y: 0, direction: DIRECTIONS.SOUTH }, { x: 4, y: 0, direction: DIRECTIONS.SOUTH }, + { x: 3, y: 1, direction: DIRECTIONS.NORTH }, { x: 4, y: 1, direction: DIRECTIONS.NORTH } ] }, [DIRECTIONS.WEST]: { @@ -84,7 +98,9 @@ export const TILES = { layout: [[1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1]], exits: [ { x: 0, y: 0, direction: DIRECTIONS.WEST }, { x: 0, y: 1, direction: DIRECTIONS.WEST }, - { x: 5, y: 0, direction: DIRECTIONS.EAST }, { x: 5, y: 1, direction: DIRECTIONS.EAST } + { x: 5, y: 0, direction: DIRECTIONS.EAST }, { x: 5, y: 1, direction: DIRECTIONS.EAST }, + { x: 3, y: 0, direction: DIRECTIONS.SOUTH }, { x: 4, y: 0, direction: DIRECTIONS.SOUTH }, + { x: 3, y: 1, direction: DIRECTIONS.NORTH }, { x: 4, y: 1, direction: DIRECTIONS.NORTH } ] } } diff --git a/src/engine/game/GameEngine.js b/src/engine/game/GameEngine.js index 962e106..b6387a2 100644 --- a/src/engine/game/GameEngine.js +++ b/src/engine/game/GameEngine.js @@ -89,14 +89,23 @@ export class GameEngine { } - isPlayerAdjacentToDoor(doorExit) { + isPlayerAdjacentToDoor(doorCells) { if (!this.player) return false; - const dx = Math.abs(this.player.x - doorExit.x); - const dy = Math.abs(this.player.y - doorExit.y); + // doorCells should be an array of {x, y} objects + // If it sends a single object, wrap it + const cells = Array.isArray(doorCells) ? doorCells : [doorCells]; - // Adjacent means distance of 1 in one direction and 0 in the other - return (dx === 1 && dy === 0) || (dx === 0 && dy === 1); + for (const cell of cells) { + const dx = Math.abs(this.player.x - cell.x); + const dy = Math.abs(this.player.y - cell.y); + + // Adjacent means distance of 1 in one direction and 0 in the other + if ((dx === 1 && dy === 0) || (dx === 0 && dy === 1)) { + return true; + } + } + return false; } update(time) { diff --git a/src/main.js b/src/main.js index 14e4291..ab8ec38 100644 --- a/src/main.js +++ b/src/main.js @@ -83,6 +83,10 @@ generator.onPlacementUpdate = (preview) => { } }; +generator.onDoorBlocked = (exitData) => { + renderer.blockDoor(exitData); +}; + // 6. Handle Clicks const handleClick = (x, y, doorMesh) => { // PRIORITY 1: Tile Placement Mode - ignore all clicks @@ -92,26 +96,33 @@ const handleClick = (x, y, doorMesh) => { } // PRIORITY 2: Door Click (must be adjacent to player) - if (doorMesh && doorMesh.userData.isDoor && !doorMesh.userData.isOpen) { - const doorExit = doorMesh.userData.cells[0]; - - if (game.isPlayerAdjacentToDoor(doorExit)) { - - - // Open door visually - renderer.openDoor(doorMesh); - - // Get proper exit data with direction - const exitData = doorMesh.userData.exitData; - if (exitData) { - generator.selectDoor(exitData); - } else { - console.error('[Main] Door missing exitData'); - } - } else { - + if (doorMesh && doorMesh.userData.isDoor) { + if (doorMesh.userData.isBlocked) { + ui.showModal('¡Derrumbe!', 'Esta puerta está bloqueada por un derrumbe. No se puede pasar.'); + return; + } + + if (!doorMesh.userData.isOpen) { + const doorExit = doorMesh.userData.cells[0]; + + if (game.isPlayerAdjacentToDoor(doorMesh.userData.cells)) { + + + // Open door visually + renderer.openDoor(doorMesh); + + // Get proper exit data with direction + const exitData = doorMesh.userData.exitData; + if (exitData) { + generator.selectDoor(exitData); + } else { + console.error('[Main] Door missing exitData'); + } + } else { + // Optional: Message if too far? + } + return; } - return; } // PRIORITY 3: Normal cell click (player selection/movement) diff --git a/src/view/CameraManager.js b/src/view/CameraManager.js index 0795c24..b4f6669 100644 --- a/src/view/CameraManager.js +++ b/src/view/CameraManager.js @@ -52,9 +52,14 @@ export class CameraManager { } centerOn(x, y) { - // Grid (x, y) -> World (x, 0, -y) + // Calculate current offset relative to OLD target + const currentOffset = this.camera.position.clone().sub(this.target); + + // Update target: Grid (x, y) -> World (x, 0, -y) this.target.set(x, 0, -y); - this.camera.position.copy(this.target).add(this.isoOffset); + + // Restore position with new target + same relative offset + this.camera.position.copy(this.target).add(currentOffset); this.camera.lookAt(this.target); } diff --git a/src/view/GameRenderer.js b/src/view/GameRenderer.js index f165c70..f3ebc51 100644 --- a/src/view/GameRenderer.js +++ b/src/view/GameRenderer.js @@ -601,6 +601,36 @@ export class GameRenderer { return false; } + blockDoor(exitData) { + if (!this.exitGroup || !exitData) return; + + // Find the door mesh + let targetDoor = null; + + for (const child of this.exitGroup.children) { + if (child.userData.isDoor) { + // Check if this door corresponds to the exitData + // exitData has x,y of one of the cells + for (const cell of child.userData.cells) { + if (cell.x === exitData.x && cell.y === exitData.y) { + targetDoor = child; + break; + } + } + } + if (targetDoor) break; + } + + if (targetDoor) { + this.getTexture('/assets/images/dungeon1/doors/door1_blocked.png', (texture) => { + targetDoor.material.map = texture; + targetDoor.material.needsUpdate = true; + targetDoor.userData.isBlocked = true; + targetDoor.userData.isOpen = false; // Ensure strictly not open + }); + } + } + // ========== MANUAL PLACEMENT SYSTEM ========== enableDoorSelection(enabled) { @@ -727,12 +757,31 @@ export class GameRenderer { }); } - // 2. GROUND PROJECTION (Green/Red) - const projectionColor = isValid ? 0x00ff00 : 0xff0000; + // 2. GROUND PROJECTION (Green/Red/Blue) + const baseColor = isValid ? 0x00ff00 : 0xff0000; + + // Calculate global exit positions + const exitKeys = new Set(); + if (preview.variant && preview.variant.exits) { + preview.variant.exits.forEach(ex => { + const gx = x + ex.x; + const gy = y + ex.y; + exitKeys.add(`${gx},${gy}`); + }); + } + cells.forEach(cell => { + const key = `${cell.x},${cell.y}`; + let color = baseColor; + + // If this cell is an exit, color it Blue + if (exitKeys.has(key)) { + color = 0x0000ff; // Blue + } + const geometry = new THREE.PlaneGeometry(0.95, 0.95); const material = new THREE.MeshBasicMaterial({ - color: projectionColor, + color: color, transparent: true, opacity: 0.5, side: THREE.DoubleSide diff --git a/src/view/UIManager.js b/src/view/UIManager.js index 427b194..4a52f64 100644 --- a/src/view/UIManager.js +++ b/src/view/UIManager.js @@ -224,7 +224,6 @@ export class UIManager { }; placementControls.appendChild(this.rotateBtn); - // Place button this.placeBtn = document.createElement('button'); this.placeBtn.textContent = '⬇ Bajar'; this.placeBtn.style.padding = '10px 20px'; @@ -243,6 +242,29 @@ export class UIManager { } }; 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) { @@ -351,4 +373,141 @@ export class UIManager { 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); + } }