diff --git a/src/main.js b/src/main.js index eca5071..7927abc 100644 --- a/src/main.js +++ b/src/main.js @@ -1,7 +1,7 @@ import './style.css'; import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; -import { io } from "socket.io-client"; +// import { io } from "socket.io-client"; import { turnManager, PHASES } from './systems/TurnManager.js'; import { dungeonDeck } from './dungeon/DungeonDecks.js'; import * as TileDefinitions from './dungeon/TileDefinitions.js'; @@ -11,7 +11,13 @@ import { TILE_DEFINITIONS, getTileDefinition } from './dungeon/TileDefinitions.j // --- NETWORK SETUP --- // Dynamic connection to support playing from mobile on the same network const socketUrl = `http://${window.location.hostname}:3001`; -const socket = io(socketUrl); +// const socket = io(socketUrl); +// MOCK SOCKET to avoid connection errors while debugging +const socket = { + on: (event, callback) => console.log(`[MockSocket] Listening for ${event}`), + emit: (event, data) => console.log(`[MockSocket] Emitting ${event}`, data), + id: 'mock-id' +}; let lobbyCode = null; socket.on("connect", () => { @@ -101,6 +107,35 @@ function generateDungeon() { }; } +// Helper function to create a flipped version of a tile +function createFlippedTile(tileDef, direction) { + const flipped = { ...tileDef }; + + // Create a new flipped walkability matrix + const { walkability, width, height } = tileDef; + const newWalkability = []; + + if (direction === 'horizontal') { + // Flip horizontally (mirror left-right) + for (let y = 0; y < height; y++) { + newWalkability[y] = [...walkability[y]].reverse(); + } + flipped.id = tileDef.id + '_flipped_h'; + } else { + // Flip vertically (mirror top-bottom) + for (let y = 0; y < height; y++) { + newWalkability[y] = walkability[height - 1 - y]; + } + flipped.id = tileDef.id + '_flipped_v'; + } + + flipped.walkability = newWalkability; + // Note: We keep the same image, the flip is only for walkability logic + // If you want visual flipping, you'd need to modify the rendering code + + return flipped; +} + // --- EXPLORACIÓN DINÁMICA --- function exploreRoom(originRoom, door) { // Draw an event card @@ -119,7 +154,7 @@ function exploreRoom(originRoom, door) { // Try to draw a compatible abstract tile type (up to 10 attempts) let card = null; - let alignmentOffset = 0; + let placementOffset = 0; // Offset for L-shapes based on selection let attempts = 0; const maxAttempts = 10; @@ -151,34 +186,111 @@ function exploreRoom(originRoom, door) { }); break; case 'L': - candidates = [...TileDefinitions.L_SHAPES]; + // NEW RULE: Select L-shape based on corridor exit direction + if (door.side === 'N') { + // North exit (vertical corridor going up): choose LSE or LSW + const lOptions = [ + { tile: TileDefinitions.TILE_DEFINITIONS.L_SE, offset: -1 }, // LSE with offset -1 + { tile: TileDefinitions.TILE_DEFINITIONS.L_WS, offset: 1 } // LSW with offset +1 + ]; + const selected = lOptions[Math.floor(Math.random() * lOptions.length)]; + card = selected.tile; + placementOffset = selected.offset; + console.log(`✓ Selected ${card.id} for N exit with offset ${placementOffset}`); + } else if (door.side === 'S') { + // South exit (vertical corridor going down): choose LNE or LNW + const lOptions = [ + { tile: TileDefinitions.TILE_DEFINITIONS.L_NE, offset: 1 }, // LNE with offset 0 + { tile: TileDefinitions.TILE_DEFINITIONS.L_WN, offset: -1 } // LNW with offset +2 + ]; + const selected = lOptions[Math.floor(Math.random() * lOptions.length)]; + card = selected.tile; + placementOffset = selected.offset; + console.log(`✓ Selected ${card.id} for S exit with offset ${placementOffset}`); + } else if (door.side === 'E') { + // East exit: choose LWN or LWS + const lOptions = [ + { tile: TileDefinitions.TILE_DEFINITIONS.L_WN, offset: 1 }, // LWN with offset 0 + { tile: TileDefinitions.TILE_DEFINITIONS.L_WS, offset: -1 } // LWS with offset +2 + ]; + const selected = lOptions[Math.floor(Math.random() * lOptions.length)]; + card = selected.tile; + placementOffset = selected.offset; + console.log(`✓ Selected ${card.id} for E exit with offset ${placementOffset}`); + } else if (door.side === 'W') { + // West exit: choose LNE or LSE + const lOptions = [ + { tile: TileDefinitions.TILE_DEFINITIONS.L_NE, offset: 1 }, // LNE with offset +2 + { tile: TileDefinitions.TILE_DEFINITIONS.L_SE, offset: -1 } // LSE with offset 0 + ]; + const selected = lOptions[Math.floor(Math.random() * lOptions.length)]; + card = selected.tile; + placementOffset = selected.offset; + console.log(`✓ Selected ${card.id} for W exit with offset ${placementOffset}`); + } break; case 'T': - candidates = [...TileDefinitions.T_JUNCTIONS]; + // T-junction placement based on corridor exit direction + if (door.side === 'N') { + // North exit: choose T_NES, T_WNS, or T_WSE + const tOptions = [ + { tile: TileDefinitions.TILE_DEFINITIONS.T_NES, offset: -1 }, // T_NES with offset -1 + { tile: TileDefinitions.TILE_DEFINITIONS.T_WNS, offset: 1 }, // T_WNS with offset +1 + { tile: TileDefinitions.TILE_DEFINITIONS.T_WSE, offset: 0 } // T_WSE with offset 0 (placeholder) + ]; + const selected = tOptions[Math.floor(Math.random() * tOptions.length)]; + card = selected.tile; + placementOffset = selected.offset; + console.log(`✓ Selected ${card.id} for N exit with offset ${placementOffset}`); + } else { + // For other exits, use all T-junctions (fallback) + candidates = [...TileDefinitions.T_JUNCTIONS]; + } break; } + // If we already selected a specific card (L-shapes with N/S exits), skip validation + if (card) { + break; + } + if (candidates.length === 0) { console.warn(`No candidates found for type ${abstractCard.type}`); attempts++; continue; } - // Try all candidates and collect those that fit + // For rooms: randomly select one and optionally flip it + if (abstractCard.type === 'room_4x4' || abstractCard.type === 'room_4x6') { + const selectedRoom = candidates[Math.floor(Math.random() * candidates.length)]; + + // Randomly decide to flip horizontally or vertically + const shouldFlip = Math.random() < 0.5; + const flipDirection = Math.random() < 0.5 ? 'horizontal' : 'vertical'; + + if (shouldFlip) { + card = createFlippedTile(selectedRoom, flipDirection); + console.log(`✓ Selected ${selectedRoom.id} and flipped ${flipDirection}`); + } else { + card = selectedRoom; + console.log(`✓ Selected ${selectedRoom.id} without flipping`); + } + break; + } + + // For corridors and T-junctions: validate and select const fittingVariants = []; for (const variant of candidates) { const connectionResult = canConnectTiles(originRoom, variant, door.side); if (connectionResult.valid) { - fittingVariants.push({ variant, offset: connectionResult.offset }); + fittingVariants.push(variant); } } if (fittingVariants.length > 0) { // RANDOM selection from fitting variants - const selected = fittingVariants[Math.floor(Math.random() * fittingVariants.length)]; - card = selected.variant; - alignmentOffset = selected.offset; - console.log(`✓ Selected ${card.id} (${card.tileType}) randomly from ${fittingVariants.length} fitting variants, offset ${alignmentOffset}`); + card = fittingVariants[Math.floor(Math.random() * fittingVariants.length)]; + console.log(`✓ Selected ${card.id} (${card.tileType}) randomly from ${fittingVariants.length} fitting variants`); } else { console.log(`✗ No ${abstractCard.type} variant fits, trying another tile type...`); attempts++; @@ -193,35 +305,33 @@ function exploreRoom(originRoom, door) { const nextTileDef = card; const newRoomId = ROOMS.rooms.length + 1; - // Calculate entry door position in the new tile + // Calculate entry door position relative to the new tile origin + // We use external adjacent coordinates (-1 or width/height) to place the new tile + // strictly adjacent to the previous one, preventing 1-cell overlaps. let entryGridX, entryGridY; + // Use explicit door coordinates if available (for asymmetric tiles like L/T) + const doorCoords = nextTileDef.doorCoordinates ? nextTileDef.doorCoordinates[entrySide] : null; + if (entrySide === 'N') { - entryGridX = Math.floor(nextTileDef.width / 2); - entryGridY = 0; + entryGridX = doorCoords ? doorCoords.x : Math.floor(nextTileDef.width / 2); + entryGridY = -1; } else if (entrySide === 'S') { - entryGridX = Math.floor(nextTileDef.width / 2); + entryGridX = doorCoords ? doorCoords.x : Math.floor(nextTileDef.width / 2); entryGridY = nextTileDef.height; } else if (entrySide === 'E') { entryGridX = nextTileDef.width; - entryGridY = Math.floor(nextTileDef.height / 2); + entryGridY = doorCoords ? doorCoords.y : Math.floor(nextTileDef.height / 2); } else if (entrySide === 'W') { - entryGridX = 0; - entryGridY = Math.floor(nextTileDef.height / 2); + entryGridX = -1; + entryGridY = doorCoords ? doorCoords.y : Math.floor(nextTileDef.height / 2); } // Calculate absolute position for the new tile let newX = originRoom.tile.x + door.gridX - entryGridX; let newY = originRoom.tile.y + door.gridY - entryGridY; - // Apply alignment offset based on exit direction - if (door.side === 'N' || door.side === 'S') { - // Vertical connection - offset horizontally - newX += alignmentOffset; - } else { - // Horizontal connection - offset vertically - newY += alignmentOffset; - } + // Manual placementOffset removed: Handled by explicit doorCoordinates via TileDefinitions.js // Check for collisions if (!isAreaFree(newX, newY, nextTileDef.width, nextTileDef.height)) { @@ -258,27 +368,37 @@ function exploreRoom(originRoom, door) { nextTileDef.exits.forEach(exitDir => { if (exitDir === entrySide) return; // Already have this connection + // Check if this is an L or T tile to make exit open by default + // For L and T tiles, exits are open passages, not closed doors + const isOpenPassage = (nextTileDef.tileType === 'L' || nextTileDef.tileType === 'T'); + const exitDoor = { side: exitDir, leadsTo: null, - isOpen: false, - isDoor: true, // Will be determined when connected + isOpen: isOpenPassage, // Open by default for L/T + isDoor: !isOpenPassage, // Not a blocking door for L/T id: `door_${newRoomId}_${exitDir}` }; - // Calculate door coordinates - if (exitDir === 'N') { - exitDoor.gridX = Math.floor(nextTileDef.width / 2); - exitDoor.gridY = 0; - } else if (exitDir === 'S') { - exitDoor.gridX = Math.floor(nextTileDef.width / 2); - exitDoor.gridY = nextTileDef.height; - } else if (exitDir === 'E') { - exitDoor.gridX = nextTileDef.width; - exitDoor.gridY = Math.floor(nextTileDef.height / 2); - } else if (exitDir === 'W') { - exitDoor.gridX = 0; - exitDoor.gridY = Math.floor(nextTileDef.height / 2); + // Calculate door coordinates using explicit definition or default centering + if (nextTileDef.doorCoordinates && nextTileDef.doorCoordinates[exitDir]) { + exitDoor.gridX = nextTileDef.doorCoordinates[exitDir].x; + exitDoor.gridY = nextTileDef.doorCoordinates[exitDir].y; + } else { + // Default centering logic + if (exitDir === 'N') { + exitDoor.gridX = Math.floor(nextTileDef.width / 2); + exitDoor.gridY = 0; + } else if (exitDir === 'S') { + exitDoor.gridX = Math.floor(nextTileDef.width / 2); + exitDoor.gridY = nextTileDef.height - 1; + } else if (exitDir === 'E') { + exitDoor.gridX = nextTileDef.width - 1; + exitDoor.gridY = Math.floor(nextTileDef.height / 2); + } else if (exitDir === 'W') { + exitDoor.gridX = 0; + exitDoor.gridY = Math.floor(nextTileDef.height / 2); + } } newRoom.doors.push(exitDoor); @@ -743,7 +863,7 @@ function shouldPlaceDoor(tileTypeA, tileTypeB) { } // Validate walkability alignment between two tiles at their connection point -// Returns: { valid: boolean, offset: number } - offset is how much to shift tileB to align with tileA +// Returns: { valid: boolean } - whether the tiles can connect function validateWalkabilityAlignment(tileDefA, posA, tileDefB, posB, exitSide) { // Get the edge cells that will connect const edgeA = getEdgeCells(tileDefA, exitSide); @@ -753,101 +873,18 @@ function validateWalkabilityAlignment(tileDefA, posA, tileDefB, posB, exitSide) console.log(`[ALIGN] EdgeA (${tileDefA.id}):`, edgeA); console.log(`[ALIGN] EdgeB (${tileDefB.id}):`, edgeB); - // Special handling for corridor connections - // Corridors are 2 tiles wide, rooms/L/T are typically 4 tiles wide - // We need to find where the corridor's walkable area aligns with the room's walkable area + // For now, we just check that both edges have at least some walkable cells + // The actual alignment will be handled manually by the user + const hasWalkableA = edgeA.some(cell => cell > 0); + const hasWalkableB = edgeB.some(cell => cell > 0); - if (edgeA.length !== edgeB.length) { - // Different edge lengths - need to find alignment - const smallerEdge = edgeA.length < edgeB.length ? edgeA : edgeB; - const largerEdge = edgeA.length < edgeB.length ? edgeB : edgeA; - const isASmaller = edgeA.length < edgeB.length; - - console.log(`[ALIGN] Different sizes: ${edgeA.length} vs ${edgeB.length}`); - console.log(`[ALIGN] isASmaller: ${isASmaller}`); - - // Find walkable cells in smaller edge - const smallerWalkable = smallerEdge.filter(cell => cell > 0); - if (smallerWalkable.length === 0) { - console.warn('[ALIGN] No walkable cells in smaller edge'); - return { valid: false, offset: 0 }; - } - - const smallerWidth = smallerEdge.length; - const largerWidth = largerEdge.length; - - // FIRST: Try offset 0 (no displacement needed) - let validAtZero = true; - for (let i = 0; i < smallerWidth; i++) { - const smallCell = smallerEdge[i]; - const largeCell = largerEdge[i]; - - const isSmallWalkable = smallCell > 0; - const isLargeWalkable = largeCell > 0; - - console.log(`[ALIGN] Offset 0, index ${i}: small=${smallCell}(${isSmallWalkable}) vs large=${largeCell}(${isLargeWalkable})`); - - if (isSmallWalkable !== isLargeWalkable) { - validAtZero = false; - console.log(`[ALIGN] ❌ Offset 0 FAILED at index ${i}: walkability mismatch`); - break; - } - } - - if (validAtZero) { - console.log(`✓ [ALIGN] Valid alignment at offset 0 (no displacement needed)`); - return { valid: true, offset: 0 }; - } - - // If offset 0 doesn't work, try offset of 2 (corridor width) - // This aligns the corridor with the other walkable section of the L/T - const offset = 2; - if (offset <= largerWidth - smallerWidth) { - let valid = true; - for (let i = 0; i < smallerWidth; i++) { - const smallCell = smallerEdge[i]; - const largeCell = largerEdge[offset + i]; - - const isSmallWalkable = smallCell > 0; - const isLargeWalkable = largeCell > 0; - - if (isSmallWalkable !== isLargeWalkable) { - valid = false; - console.log(`[ALIGN] Offset ${offset} failed at index ${i}: ${smallCell} vs ${largeCell}`); - break; - } - } - - if (valid) { - // Calculate final offset - // If A (corridor) is smaller than B (L/T), we need to shift B by +offset - // If B is smaller than A, we need to shift B by -offset - const finalOffset = isASmaller ? offset : -offset; - console.log(`✓ [ALIGN] Valid alignment at offset ${offset}, final offset: ${finalOffset} (isASmaller: ${isASmaller})`); - return { valid: true, offset: finalOffset }; - } - } - - console.warn('[ALIGN] Could not find valid alignment for edges of different sizes'); - return { valid: false, offset: 0 }; + if (!hasWalkableA || !hasWalkableB) { + console.warn('[ALIGN] One or both edges have no walkable cells'); + return { valid: false }; } - // Same length - check direct alignment - for (let i = 0; i < edgeA.length; i++) { - const cellA = edgeA[i]; - const cellB = edgeB[i]; - - // Rule: Cannot connect 0 (not walkable) with >0 (walkable) - const isAWalkable = cellA > 0; - const isBWalkable = cellB > 0; - - if (isAWalkable !== isBWalkable) { - console.warn(`[ALIGN] Walkability mismatch at index ${i}: ${cellA} vs ${cellB}`); - return { valid: false, offset: 0 }; - } - } - - return { valid: true, offset: 0 }; + console.log(`✓ [ALIGN] Both edges have walkable cells - connection allowed`); + return { valid: true }; } // Get edge cells from a tile definition for a given side @@ -892,7 +929,7 @@ function getOppositeSide(side) { } // Check if two tiles can connect based on type rules and walkability alignment -// Returns: { valid: boolean, offset: number } - offset for positioning the new tile +// Returns: { valid: boolean } - whether the tiles can connect function canConnectTiles(roomA, tileDefB, exitSide) { const tileDefA = roomA.tileDef; @@ -909,7 +946,7 @@ function canConnectTiles(roomA, tileDefB, exitSide) { if (!validConnections[typeA] || !validConnections[typeA].includes(typeB)) { console.warn(`Invalid connection: ${typeA} cannot connect to ${typeB}`); - return { valid: false, offset: 0 }; + return { valid: false }; } // CRITICAL: Check that tileB has an exit in the opposite direction @@ -917,17 +954,17 @@ function canConnectTiles(roomA, tileDefB, exitSide) { const requiredExit = getOppositeSide(exitSide); if (!tileDefB.exits.includes(requiredExit)) { console.warn(`Exit direction mismatch: ${tileDefB.id} doesn't have required exit '${requiredExit}' (exiting via '${exitSide}')`); - return { valid: false, offset: 0 }; + return { valid: false }; } - // Check walkability alignment and get offset + // Check walkability alignment const alignmentResult = validateWalkabilityAlignment(tileDefA, roomA.tile, tileDefB, null, exitSide); if (!alignmentResult.valid) { console.warn('Walkability alignment failed'); - return { valid: false, offset: 0 }; + return { valid: false }; } - return alignmentResult; + return { valid: true }; } // --- CREACIÓN DE MARCADORES --- @@ -1274,9 +1311,77 @@ async function animateMovement() { SESSION.selectedUnitId = null; // Deseleccionar unidad al terminar de mover updateSelectionVisuals(); SESSION.isAnimating = false; + + // Auto-exploration check for open passages (L/T tiles) + checkAutoExploration(unit); + drawMinimap(); // Actualizar posición final del jugador } +// Check if unit is on an open passage leading to unexplored area +function checkAutoExploration(unit) { + // Find unit's current room + const currentRoom = ROOMS.rooms.find(r => r.entities.some(e => e.id === unit.id)); + if (!currentRoom) return; + + // DEBUG: Log unit position for troubleshooting + console.log(`[AutoExplore] Unit at (${unit.x}, ${unit.y}) in Room ${currentRoom.id}`); + + const door = currentRoom.doors.find(d => { + const doorGlobalX = currentRoom.tile.x + d.gridX; + const doorGlobalY = currentRoom.tile.y + d.gridY; + + // DEBUG: Log each door check + console.log(`[CheckDoor] ${d.id} (${d.side}): Global(${doorGlobalX}, ${doorGlobalY}) vs Unit(${unit.x}, ${unit.y})`); + + // Check 1: Is Unit on the door line? (Internal or External adjacent) + let isMainAxisMatch = false; + let isCrossAxisMatch = false; + + if (d.side === 'N' || d.side === 'S') { + // Main axis = Y. Internal: doorGlobalY. External: +/- 1 + const deltaY = unit.y - doorGlobalY; + // Allow 0 (internal) or 1 step out (external) + if (d.side === 'N') isMainAxisMatch = (deltaY === 0 || deltaY === -1); + else isMainAxisMatch = (deltaY === 0 || deltaY === 1); + + // Cross axis = X. Allow +/- 1 for corridor width + isCrossAxisMatch = Math.abs(unit.x - doorGlobalX) <= 1; + + } else { // E or W + // Main axis = X. Internal: doorGlobalX. External: +/- 1 + const deltaX = unit.x - doorGlobalX; + // Allow 0 (internal) or 1 step out (external) + if (d.side === 'W') isMainAxisMatch = (deltaX === 0 || deltaX === -1); + else isMainAxisMatch = (deltaX === 0 || deltaX === 1); + + // Cross axis = Y. Allow +/- 1 for corridor width + isCrossAxisMatch = Math.abs(unit.y - doorGlobalY) <= 1; + } + + if (isMainAxisMatch && isCrossAxisMatch) { + console.log(`[AutoExplore] MATCH! Door ${d.id} (${d.side}). Unit(${unit.x}, ${unit.y}) vs Door(${doorGlobalX}, ${doorGlobalY})`); + return true; + } + return false; + }); + + if (door) { + console.log(`[AutoExplore] Found door at unit pos: ${door.id}, Open: ${door.isOpen}, LeadsTo: ${door.leadsTo}`); + + if (door.isOpen && door.leadsTo === null) { + console.log("[AutoExplore] Triggering exploration..."); + // Use timeout to allow movement animation to settle/feel natural + setTimeout(() => { + const newRoom = exploreRoom(currentRoom, door); + if (newRoom) { + renderRoom(newRoom); + } + }, 100); + } + } +} + // Verifica si la unidad ha entrado físicamente en una sala diferente a la registrada function detectRoomChange(unit, currentLogicalRoom) { for (const room of ROOMS.rooms) { @@ -1520,6 +1625,14 @@ async function renderRoom(room) { for (const config of wallConfigs) { const wallSide = config.side; + + // Skip wall rendering for L and T tiles on their exit sides + if ((room.tileDef.tileType === 'L' || room.tileDef.tileType === 'T') && + room.tileDef.exits.includes(wallSide)) { + console.log(`Skipping wall on ${wallSide} for ${room.tileDef.tileType} tile (exit side)`); + continue; // Don't render wall on exit sides + } + const door = doorsOnSides[wallSide]; // Función helper para crear un segmento de pared