Implement tile discarding, blocked doors, and correct corridor exits

- 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.
This commit is contained in:
2026-01-02 23:48:42 +01:00
parent 8bb0dd8780
commit ac536ac96c
8 changed files with 312 additions and 38 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

View File

@@ -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
*/

View File

@@ -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 }
]
}
}

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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);
}
}