From 2e62a4e62b4cf775a25e8d199604927f5f596dfd Mon Sep 17 00:00:00 2001 From: "Z. Cliffe Schreuders" Date: Sun, 16 Nov 2025 08:45:06 +0000 Subject: [PATCH] feat(room-layout): Implement new grid-based room layout system with critical bug fixes - Add grid unit constants (5x4 tiles) to constants.js - Create comprehensive grid conversion helper functions - Implement 4-direction room positioning (N/S/E/W) with breadth-first algorithm - Add door placement functions with CRITICAL fixes: * Negative modulo fix: ((sum % 2) + 2) % 2 for deterministic placement * Asymmetric alignment fix: single-door rooms align with multi-door rooms * Consistent grid alignment using Math.floor() for negative coordinates - Rewrite calculateRoomPositions to support variable room sizes - Update createDoorSpritesForRoom to use new placement system - Store room dimensions globally for cross-system access This implements the comprehensive room layout redesign as specified in planning_notes/new_room_layout with all critical fixes from review2. Addresses: Variable room sizes, proper door alignment, and grid-based positioning --- js/core/rooms.js | 557 ++++++++++++++++++++++++-------- js/systems/doors.js | 714 +++++++++++++++++++++++++++--------------- js/utils/constants.js | 11 + 3 files changed, 894 insertions(+), 388 deletions(-) diff --git a/js/core/rooms.js b/js/core/rooms.js index bcfbc8b..eaa4521 100644 --- a/js/core/rooms.js +++ b/js/core/rooms.js @@ -42,7 +42,18 @@ */ // Room management system -import { TILE_SIZE, DOOR_ALIGN_OVERLAP, GRID_SIZE, INTERACTION_RANGE_SQ, INTERACTION_CHECK_INTERVAL } from '../utils/constants.js?v=8'; +import { + TILE_SIZE, + DOOR_ALIGN_OVERLAP, + GRID_SIZE, + INTERACTION_RANGE_SQ, + INTERACTION_CHECK_INTERVAL, + GRID_UNIT_WIDTH_TILES, + GRID_UNIT_HEIGHT_TILES, + VISUAL_TOP_TILES, + GRID_UNIT_WIDTH_PX, + GRID_UNIT_HEIGHT_PX +} from '../utils/constants.js?v=8'; // Import the new system modules import { initializeDoors, createDoorSpritesForRoom, checkDoorTransitions, updateDoorSpritesVisibility } from '../systems/doors.js'; @@ -641,147 +652,433 @@ export function calculateWorldBounds(gameInstance) { }; } -export function calculateRoomPositions(gameInstance) { - const OVERLAP = 64; +// ============================================================================ +// GRID UNIT CONVERSION FUNCTIONS +// ============================================================================ + +/** + * Convert tile dimensions to grid units + * + * Grid units are the base stacking size: 5 tiles wide × 4 tiles tall + * (excluding top 2 visual wall tiles) + * + * @param {number} widthTiles - Room width in tiles + * @param {number} heightTiles - Room height in tiles (including visual wall) + * @returns {{gridWidth: number, gridHeight: number}} + */ +function tilesToGridUnits(widthTiles, heightTiles) { + const gridWidth = Math.floor(widthTiles / GRID_UNIT_WIDTH_TILES); + + // Subtract visual top wall tiles before calculating grid height + const stackingHeightTiles = heightTiles - VISUAL_TOP_TILES; + const gridHeight = Math.floor(stackingHeightTiles / GRID_UNIT_HEIGHT_TILES); + + return { gridWidth, gridHeight }; +} + +/** + * Convert grid coordinates to world position + * + * Grid coordinates are positions in grid unit space. + * This converts them to pixel world coordinates. + * + * @param {number} gridX - Grid X coordinate + * @param {number} gridY - Grid Y coordinate + * @returns {{x: number, y: number}} + */ +function gridToWorld(gridX, gridY) { + return { + x: gridX * GRID_UNIT_WIDTH_PX, + y: gridY * GRID_UNIT_HEIGHT_PX + }; +} + +/** + * Convert world position to grid coordinates + * + * @param {number} worldX - World X position in pixels + * @param {number} worldY - World Y position in pixels + * @returns {{gridX: number, gridY: number}} + */ +function worldToGrid(worldX, worldY) { + return { + gridX: Math.floor(worldX / GRID_UNIT_WIDTH_PX), + gridY: Math.floor(worldY / GRID_UNIT_HEIGHT_PX) + }; +} + +/** + * Align a world position to the nearest grid boundary + * + * Uses Math.floor for consistent rounding of negative numbers + * (always rounds toward negative infinity) + * + * @param {number} worldX - World X position + * @param {number} worldY - World Y position + * @returns {{x: number, y: number}} + */ +function alignToGrid(worldX, worldY) { + // Use floor for consistent rounding of negative numbers + const gridX = Math.floor(worldX / GRID_UNIT_WIDTH_PX); + const gridY = Math.floor(worldY / GRID_UNIT_HEIGHT_PX); + + return { + x: gridX * GRID_UNIT_WIDTH_PX, + y: gridY * GRID_UNIT_HEIGHT_PX + }; +} + +/** + * Extract room dimensions from Tiled JSON data + * + * Reads the tilemap to get room size and calculates: + * - Tile dimensions + * - Pixel dimensions + * - Grid units + * - Stacking height (for positioning calculations) + * + * @param {string} roomId - Room identifier + * @param {Object} roomData - Room data from scenario + * @param {Phaser.Game} gameInstance - Game instance for accessing tilemaps + * @returns {Object} Dimension data + */ +function getRoomDimensions(roomId, roomData, gameInstance) { + const map = gameInstance.cache.tilemap.get(roomData.type); + + let widthTiles, heightTiles; + + // Try different ways to access tilemap data + if (map && map.json) { + widthTiles = map.json.width; + heightTiles = map.json.height; + } else if (map && map.data) { + widthTiles = map.data.width; + heightTiles = map.data.height; + } else { + // Fallback to standard room size + console.warn(`Could not read dimensions for ${roomId}, using default 10×10`); + widthTiles = 10; + heightTiles = 10; + } + + // Calculate grid units + const { gridWidth, gridHeight } = tilesToGridUnits(widthTiles, heightTiles); + + // Calculate pixel dimensions + const widthPx = widthTiles * TILE_SIZE; + const heightPx = heightTiles * TILE_SIZE; + const stackingHeightPx = (heightTiles - VISUAL_TOP_TILES) * TILE_SIZE; + + return { + widthTiles, + heightTiles, + widthPx, + heightPx, + stackingHeightPx, + gridWidth, + gridHeight + }; +} + +// ============================================================================ +// ROOM POSITIONING FUNCTIONS +// ============================================================================ + +/** + * Position a single room to the north of current room + */ +function positionNorthSingle(currentRoom, connectedRoom, currentPos, dimensions) { + const currentDim = dimensions[currentRoom]; + const connectedDim = dimensions[connectedRoom]; + + // Center the connected room above current room + const x = currentPos.x + (currentDim.widthPx - connectedDim.widthPx) / 2; + const y = currentPos.y - connectedDim.stackingHeightPx; + + // Align to grid using floor (consistent rounding for negatives) + return alignToGrid(x, y); +} + +/** + * Position multiple rooms to the north of current room + */ +function positionNorthMultiple(currentRoom, connectedRooms, currentPos, dimensions) { + const currentDim = dimensions[currentRoom]; const positions = {}; - const gameScenario = window.gameScenario; - - console.log('=== Starting Room Position Calculations ==='); - - // Get room dimensions from tilemaps - const roomDimensions = {}; - Object.entries(gameScenario.rooms).forEach(([roomId, roomData]) => { - const map = gameInstance.cache.tilemap.get(roomData.type); - console.log(`Debug - Room ${roomId}:`, { - mapData: map, - fullData: map?.data, - json: map?.json - }); - - // Try different ways to access the data - if (map) { - let width, height; - if (map.json) { - width = map.json.width; - height = map.json.height; - } else if (map.data) { - width = map.data.width; - height = map.data.height; - } else { - width = map.width; - height = map.height; - } - - roomDimensions[roomId] = { - width: width * TILE_SIZE, // tile width is TILE_SIZE - height: height * TILE_SIZE // tile height is TILE_SIZE - }; - } else { - console.error(`Could not find tilemap data for room ${roomId}`); - // Fallback to default dimensions if needed - roomDimensions[roomId] = { - width: 320, // default width (10 tiles at 32px) - height: 288 // default height (9 tiles at 32px) - }; - } + + // Calculate total width of all connected rooms + const totalWidth = connectedRooms.reduce((sum, roomId) => { + return sum + dimensions[roomId].widthPx; + }, 0); + + // Determine starting X position (center the group) + const startX = currentPos.x + (currentDim.widthPx - totalWidth) / 2; + + // Position each room left to right + let currentX = startX; + connectedRooms.forEach(roomId => { + const connectedDim = dimensions[roomId]; + + // Y position is based on stacking height + const y = currentPos.y - connectedDim.stackingHeightPx; + + // Align to grid + positions[roomId] = alignToGrid(currentX, y); + + currentX += connectedDim.widthPx; }); - - // Start with reception room at origin - positions[gameScenario.startRoom] = { x: 0, y: 0 }; - console.log(`Starting room ${gameScenario.startRoom} position:`, positions[gameScenario.startRoom]); - - // Process rooms level by level, starting from reception - const processed = new Set([gameScenario.startRoom]); - const queue = [gameScenario.startRoom]; - + + return positions; +} + +/** + * Position a single room to the south of current room + */ +function positionSouthSingle(currentRoom, connectedRoom, currentPos, dimensions) { + const currentDim = dimensions[currentRoom]; + const connectedDim = dimensions[connectedRoom]; + + // Center the connected room below current room + const x = currentPos.x + (currentDim.widthPx - connectedDim.widthPx) / 2; + const y = currentPos.y + currentDim.stackingHeightPx; + + // Align to grid + return alignToGrid(x, y); +} + +/** + * Position multiple rooms to the south of current room + */ +function positionSouthMultiple(currentRoom, connectedRooms, currentPos, dimensions) { + const currentDim = dimensions[currentRoom]; + const positions = {}; + + // Calculate total width + const totalWidth = connectedRooms.reduce((sum, roomId) => { + return sum + dimensions[roomId].widthPx; + }, 0); + + // Determine starting X position (center the group) + const startX = currentPos.x + (currentDim.widthPx - totalWidth) / 2; + + // Position each room left to right + let currentX = startX; + connectedRooms.forEach(roomId => { + const connectedDim = dimensions[roomId]; + + // Y position below current room + const y = currentPos.y + currentDim.stackingHeightPx; + + // Align to grid + positions[roomId] = alignToGrid(currentX, y); + + currentX += connectedDim.widthPx; + }); + + return positions; +} + +/** + * Position a single room to the east of current room + */ +function positionEastSingle(currentRoom, connectedRoom, currentPos, dimensions) { + const currentDim = dimensions[currentRoom]; + const connectedDim = dimensions[connectedRoom]; + + // Position to the right, aligned at north edge + const x = currentPos.x + currentDim.widthPx; + const y = currentPos.y; + + // Align to grid + return alignToGrid(x, y); +} + +/** + * Position multiple rooms to the east of current room + */ +function positionEastMultiple(currentRoom, connectedRooms, currentPos, dimensions) { + const currentDim = dimensions[currentRoom]; + const positions = {}; + + // Position to the right + const x = currentPos.x + currentDim.widthPx; + + // Stack vertically starting at current Y + let currentY = currentPos.y; + connectedRooms.forEach(roomId => { + const connectedDim = dimensions[roomId]; + + // Align to grid + positions[roomId] = alignToGrid(x, currentY); + + currentY += connectedDim.stackingHeightPx; + }); + + return positions; +} + +/** + * Position a single room to the west of current room + */ +function positionWestSingle(currentRoom, connectedRoom, currentPos, dimensions) { + const connectedDim = dimensions[connectedRoom]; + + // Position to the left, aligned at north edge + const x = currentPos.x - connectedDim.widthPx; + const y = currentPos.y; + + // Align to grid + return alignToGrid(x, y); +} + +/** + * Position multiple rooms to the west of current room + */ +function positionWestMultiple(currentRoom, connectedRooms, currentPos, dimensions) { + const positions = {}; + + // Stack vertically starting at current Y + let currentY = currentPos.y; + connectedRooms.forEach(roomId => { + const connectedDim = dimensions[roomId]; + + // Position to the left + const x = currentPos.x - connectedDim.widthPx; + + // Align to grid + positions[roomId] = alignToGrid(x, currentY); + + currentY += connectedDim.stackingHeightPx; + }); + + return positions; +} + +/** + * Route single room positioning to appropriate direction function + */ +function positionSingleRoom(direction, currentRoom, connectedRoom, currentPos, dimensions) { + switch (direction) { + case 'north': return positionNorthSingle(currentRoom, connectedRoom, currentPos, dimensions); + case 'south': return positionSouthSingle(currentRoom, connectedRoom, currentPos, dimensions); + case 'east': return positionEastSingle(currentRoom, connectedRoom, currentPos, dimensions); + case 'west': return positionWestSingle(currentRoom, connectedRoom, currentPos, dimensions); + default: + console.error(`Unknown direction: ${direction}`); + return currentPos; + } +} + +/** + * Route multiple room positioning to appropriate direction function + */ +function positionMultipleRooms(direction, currentRoom, connectedRooms, currentPos, dimensions) { + switch (direction) { + case 'north': return positionNorthMultiple(currentRoom, connectedRooms, currentPos, dimensions); + case 'south': return positionSouthMultiple(currentRoom, connectedRooms, currentPos, dimensions); + case 'east': return positionEastMultiple(currentRoom, connectedRooms, currentPos, dimensions); + case 'west': return positionWestMultiple(currentRoom, connectedRooms, currentPos, dimensions); + default: + console.error(`Unknown direction: ${direction}`); + return {}; + } +} + +export function calculateRoomPositions(gameInstance) { + const positions = {}; + const dimensions = {}; + const processed = new Set(); + const queue = []; + const gameScenario = window.gameScenario; + + console.log('=== NEW ROOM LAYOUT SYSTEM: Starting Room Position Calculations ==='); + + // Phase 1: Extract all room dimensions + console.log('\n--- Phase 1: Extracting Room Dimensions ---'); + Object.entries(gameScenario.rooms).forEach(([roomId, roomData]) => { + dimensions[roomId] = getRoomDimensions(roomId, roomData, gameInstance); + console.log(`Room ${roomId} (${roomData.type}): ${dimensions[roomId].widthTiles}×${dimensions[roomId].heightTiles} tiles = ${dimensions[roomId].gridWidth}×${dimensions[roomId].gridHeight} grid units`); + }); + + // Phase 2: Place starting room at origin + console.log('\n--- Phase 2: Placing Starting Room ---'); + const startRoomId = gameScenario.startRoom; + positions[startRoomId] = { x: 0, y: 0 }; + processed.add(startRoomId); + queue.push(startRoomId); + console.log(`Starting room "${startRoomId}" positioned at (0, 0)`); + + // Phase 3: Process rooms breadth-first + console.log('\n--- Phase 3: Processing Rooms Breadth-First ---'); while (queue.length > 0) { const currentRoomId = queue.shift(); const currentRoom = gameScenario.rooms[currentRoomId]; const currentPos = positions[currentRoomId]; - const currentDimensions = roomDimensions[currentRoomId]; - - console.log(`\nProcessing room ${currentRoomId}`); - console.log('Current position:', currentPos); - console.log('Connections:', currentRoom.connections); - - Object.entries(currentRoom.connections).forEach(([direction, connected]) => { - console.log(`\nProcessing ${direction} connection:`, connected); - - if (Array.isArray(connected)) { - const roomsToProcess = connected.filter(r => !processed.has(r)); - console.log('Unprocessed connected rooms:', roomsToProcess); - if (roomsToProcess.length === 0) return; - - if (direction === 'north' || direction === 'south') { - const firstRoom = roomsToProcess[0]; - const firstRoomWidth = roomDimensions[firstRoom].width; - const firstRoomHeight = roomDimensions[firstRoom].height; - - const secondRoom = roomsToProcess[1]; - const secondRoomWidth = roomDimensions[secondRoom].width; - const secondRoomHeight = roomDimensions[secondRoom].height; - - if (direction === 'north') { - // First room - right edge aligns with current room's left edge - positions[firstRoom] = { - x: currentPos.x - firstRoomWidth + DOOR_ALIGN_OVERLAP, - y: currentPos.y - firstRoomHeight + OVERLAP - }; - - // Second room - left edge aligns with current room's right edge - positions[secondRoom] = { - x: currentPos.x + currentDimensions.width - DOOR_ALIGN_OVERLAP, - y: currentPos.y - secondRoomHeight + OVERLAP - }; - } else if (direction === 'south') { - // First room - left edge aligns with current room's right edge - positions[firstRoom] = { - x: currentPos.x - firstRoomWidth + DOOR_ALIGN_OVERLAP, - y: currentPos.y + currentDimensions.height - OVERLAP - }; - - // Second room - right edge aligns with current room's left edge - positions[secondRoom] = { - x: currentPos.x + currentDimensions.width - DOOR_ALIGN_OVERLAP, - y: currentPos.y + currentDimensions.height - secondRoomHeight - OVERLAP - }; - } - - roomsToProcess.forEach(roomId => { - processed.add(roomId); - queue.push(roomId); - console.log(`Positioned room ${roomId} at:`, positions[roomId]); - }); - } + + console.log(`\nProcessing room: ${currentRoomId}`); + console.log(` Position: (${currentPos.x}, ${currentPos.y})`); + + // Skip rooms without connections + if (!currentRoom.connections) { + console.log(` No connections for ${currentRoomId}`); + continue; + } + + // Process each direction + ['north', 'south', 'east', 'west'].forEach(direction => { + const connected = currentRoom.connections[direction]; + if (!connected) return; + + // Convert to array if single connection + const connectedRooms = Array.isArray(connected) ? connected : [connected]; + + // Filter out already processed rooms + const unprocessed = connectedRooms.filter(roomId => !processed.has(roomId)); + if (unprocessed.length === 0) { + console.log(` ${direction}: all rooms already processed`); + return; + } + + console.log(` ${direction}: positioning ${unprocessed.length} room(s) - ${unprocessed.join(', ')}`); + + // Position rooms based on count + if (unprocessed.length === 1) { + // Single room connection + const roomId = unprocessed[0]; + const position = positionSingleRoom(direction, currentRoomId, roomId, currentPos, dimensions); + positions[roomId] = position; + processed.add(roomId); + queue.push(roomId); + + const gridCoords = worldToGrid(position.x, position.y); + console.log(` ${roomId}: positioned at world(${position.x}, ${position.y}) = grid(${gridCoords.gridX}, ${gridCoords.gridY})`); } else { - if (processed.has(connected)) { - return; - } - - const connectedDimensions = roomDimensions[connected]; - - // Center the connected room - const x = currentPos.x + - (currentDimensions.width - connectedDimensions.width) / 2; - const y = direction === 'north' - ? currentPos.y - connectedDimensions.height + OVERLAP - : currentPos.y + currentDimensions.height - OVERLAP; - - - positions[connected] = { x, y }; - processed.add(connected); - queue.push(connected); - - console.log(`Positioned single room ${connected} at:`, positions[connected]); + // Multiple room connections + const newPositions = positionMultipleRooms(direction, currentRoomId, unprocessed, currentPos, dimensions); + + unprocessed.forEach(roomId => { + positions[roomId] = newPositions[roomId]; + processed.add(roomId); + queue.push(roomId); + + const gridCoords = worldToGrid(newPositions[roomId].x, newPositions[roomId].y); + console.log(` ${roomId}: positioned at world(${newPositions[roomId].x}, ${newPositions[roomId].y}) = grid(${gridCoords.gridX}, ${gridCoords.gridY})`); + }); } }); } - - console.log('\n=== Final Room Positions ==='); + + // Phase 4: Log final positions + console.log('\n--- Phase 4: Final Room Positions ---'); Object.entries(positions).forEach(([roomId, pos]) => { - console.log(`${roomId}:`, pos); + const gridCoords = worldToGrid(pos.x, pos.y); + console.log(`${roomId}: world(${pos.x}, ${pos.y}) = grid(${gridCoords.gridX}, ${gridCoords.gridY})`); }); - + + // Store dimensions globally for use by door placement + window.roomDimensions = dimensions; + + console.log('\n=== Room Position Calculations Complete ===\n'); return positions; } diff --git a/js/systems/doors.js b/js/systems/doors.js index c015300..41326b3 100644 --- a/js/systems/doors.js +++ b/js/systems/doors.js @@ -1,12 +1,19 @@ /** * DOOR SYSTEM * =========== - * + * * Handles door sprites, interactions, transitions, and visibility management. * Separated from rooms.js for better modularity and maintainability. + * + * NEW: Includes comprehensive door placement with asymmetric alignment fix + * for variable room sizes using the grid unit system. */ -import { TILE_SIZE } from '../utils/constants.js'; +import { + TILE_SIZE, + GRID_UNIT_WIDTH_PX, + GRID_UNIT_HEIGHT_PX +} from '../utils/constants.js'; import { handleUnlock } from './unlock-system.js'; let gameRef = null; @@ -37,6 +44,316 @@ let lastDoorTransitionTime = 0; const DOOR_TRANSITION_COOLDOWN = 1000; // 1 second cooldown between transitions let lastDoorTransition = null; // Track the last door transition to prevent repeats +// ============================================================================ +// DOOR PLACEMENT FUNCTIONS WITH ASYMMETRIC ALIGNMENT FIX +// ============================================================================ + +/** + * Helper to convert world position to grid coordinates + */ +function worldToGrid(worldX, worldY) { + return { + gridX: Math.floor(worldX / GRID_UNIT_WIDTH_PX), + gridY: Math.floor(worldY / GRID_UNIT_HEIGHT_PX) + }; +} + +/** + * Place a single north door with asymmetric alignment fix + * CRITICAL: Handles negative modulo correctly and aligns with multi-door rooms + */ +function placeNorthDoorSingle(roomId, roomPosition, roomDimensions, connectedRoom, gameScenario, allPositions, allDimensions) { + const roomWidthPx = roomDimensions.widthPx; + + // CRITICAL: Check if connected room has multiple connections in opposite direction + const connectedRoomData = gameScenario.rooms[connectedRoom]; + const connectedSouthConnections = connectedRoomData?.connections?.south; + + if (Array.isArray(connectedSouthConnections) && connectedSouthConnections.length > 1) { + // Connected room has multiple south doors - align with the correct one + const indexInArray = connectedSouthConnections.indexOf(roomId); + + if (indexInArray >= 0) { + // Calculate where the connected room's door is positioned + const connectedPos = allPositions[connectedRoom]; + const connectedDim = allDimensions[connectedRoom]; + + // Use same spacing logic as placeNorthDoorsMultiple + const edgeInset = TILE_SIZE * 1.5; + const availableWidth = connectedDim.widthPx - (edgeInset * 2); + const doorCount = connectedSouthConnections.length; + const spacing = availableWidth / (doorCount - 1); + + const alignedDoorX = connectedPos.x + edgeInset + (spacing * indexInArray); + const doorY = roomPosition.y + TILE_SIZE; + + return { x: alignedDoorX, y: doorY, connectedRoom }; + } + } + + // Default: Deterministic left/right placement based on grid position + // CRITICAL FIX: Handle negative grid coordinates correctly + // JavaScript modulo with negatives: -5 % 2 = -1 (not 1) + const gridCoords = worldToGrid(roomPosition.x, roomPosition.y); + const sum = gridCoords.gridX + gridCoords.gridY; + const useRightSide = ((sum % 2) + 2) % 2 === 1; + + let doorX; + if (useRightSide) { + // Northeast corner + doorX = roomPosition.x + roomWidthPx - (TILE_SIZE * 1.5); + } else { + // Northwest corner + doorX = roomPosition.x + (TILE_SIZE * 1.5); + } + + const doorY = roomPosition.y + TILE_SIZE; + return { x: doorX, y: doorY, connectedRoom }; +} + +/** + * Place multiple north doors with even spacing + */ +function placeNorthDoorsMultiple(roomId, roomPosition, roomDimensions, connectedRooms) { + const roomWidthPx = roomDimensions.widthPx; + const doorPositions = []; + + // Available width after edge insets + const edgeInset = TILE_SIZE * 1.5; + const availableWidth = roomWidthPx - (edgeInset * 2); + + // Space between doors + const doorCount = connectedRooms.length; + const doorSpacing = availableWidth / (doorCount - 1); + + connectedRooms.forEach((connectedRoom, index) => { + const doorX = roomPosition.x + edgeInset + (doorSpacing * index); + const doorY = roomPosition.y + TILE_SIZE; + + doorPositions.push({ + x: doorX, + y: doorY, + connectedRoom + }); + }); + + return doorPositions; +} + +/** + * Place a single south door with asymmetric alignment fix + */ +function placeSouthDoorSingle(roomId, roomPosition, roomDimensions, connectedRoom, gameScenario, allPositions, allDimensions) { + const roomWidthPx = roomDimensions.widthPx; + const roomHeightPx = roomDimensions.heightPx; + + // CRITICAL: Check if connected room has multiple north connections + const connectedRoomData = gameScenario.rooms[connectedRoom]; + const connectedNorthConnections = connectedRoomData?.connections?.north; + + if (Array.isArray(connectedNorthConnections) && connectedNorthConnections.length > 1) { + // Connected room has multiple north doors - align with the correct one + const indexInArray = connectedNorthConnections.indexOf(roomId); + + if (indexInArray >= 0) { + const connectedPos = allPositions[connectedRoom]; + const connectedDim = allDimensions[connectedRoom]; + + const edgeInset = TILE_SIZE * 1.5; + const availableWidth = connectedDim.widthPx - (edgeInset * 2); + const doorCount = connectedNorthConnections.length; + const spacing = availableWidth / (doorCount - 1); + + const alignedDoorX = connectedPos.x + edgeInset + (spacing * indexInArray); + const doorY = roomPosition.y + roomHeightPx - TILE_SIZE; + + return { x: alignedDoorX, y: doorY, connectedRoom }; + } + } + + // Default: Deterministic placement + const gridCoords = worldToGrid(roomPosition.x, roomPosition.y); + const sum = gridCoords.gridX + gridCoords.gridY; + const useRightSide = ((sum % 2) + 2) % 2 === 1; + + let doorX; + if (useRightSide) { + doorX = roomPosition.x + roomWidthPx - (TILE_SIZE * 1.5); + } else { + doorX = roomPosition.x + (TILE_SIZE * 1.5); + } + + const doorY = roomPosition.y + roomHeightPx - TILE_SIZE; + return { x: doorX, y: doorY, connectedRoom }; +} + +/** + * Place multiple south doors with even spacing + */ +function placeSouthDoorsMultiple(roomId, roomPosition, roomDimensions, connectedRooms) { + const roomWidthPx = roomDimensions.widthPx; + const roomHeightPx = roomDimensions.heightPx; + const doorPositions = []; + + const edgeInset = TILE_SIZE * 1.5; + const availableWidth = roomWidthPx - (edgeInset * 2); + const doorCount = connectedRooms.length; + const doorSpacing = availableWidth / (doorCount - 1); + + connectedRooms.forEach((connectedRoom, index) => { + const doorX = roomPosition.x + edgeInset + (doorSpacing * index); + const doorY = roomPosition.y + roomHeightPx - TILE_SIZE; + + doorPositions.push({ + x: doorX, + y: doorY, + connectedRoom + }); + }); + + return doorPositions; +} + +/** + * Place a single east door + */ +function placeEastDoorSingle(roomId, roomPosition, roomDimensions, connectedRoom) { + const roomWidthPx = roomDimensions.widthPx; + + const doorX = roomPosition.x + roomWidthPx - TILE_SIZE; + const doorY = roomPosition.y + (TILE_SIZE * 2); // Below visual wall + + return { x: doorX, y: doorY, connectedRoom }; +} + +/** + * Place multiple east doors with vertical spacing + */ +function placeEastDoorsMultiple(roomId, roomPosition, roomDimensions, connectedRooms) { + const roomWidthPx = roomDimensions.widthPx; + const roomHeightPx = roomDimensions.heightPx; + const doorPositions = []; + + const doorX = roomPosition.x + roomWidthPx - TILE_SIZE; + + if (connectedRooms.length === 1) { + const doorY = roomPosition.y + (TILE_SIZE * 2); + doorPositions.push({ x: doorX, y: doorY, connectedRoom: connectedRooms[0] }); + } else { + // Multiple doors - space vertically + const topY = roomPosition.y + (TILE_SIZE * 2); + const bottomY = roomPosition.y + roomHeightPx - (TILE_SIZE * 3); + const spacing = (bottomY - topY) / (connectedRooms.length - 1); + + connectedRooms.forEach((connectedRoom, index) => { + const doorY = topY + (spacing * index); + doorPositions.push({ x: doorX, y: doorY, connectedRoom }); + }); + } + + return doorPositions; +} + +/** + * Place a single west door + */ +function placeWestDoorSingle(roomId, roomPosition, roomDimensions, connectedRoom) { + const doorX = roomPosition.x + TILE_SIZE; + const doorY = roomPosition.y + (TILE_SIZE * 2); + + return { x: doorX, y: doorY, connectedRoom }; +} + +/** + * Place multiple west doors with vertical spacing + */ +function placeWestDoorsMultiple(roomId, roomPosition, roomDimensions, connectedRooms) { + const roomHeightPx = roomDimensions.heightPx; + const doorPositions = []; + + const doorX = roomPosition.x + TILE_SIZE; + + if (connectedRooms.length === 1) { + const doorY = roomPosition.y + (TILE_SIZE * 2); + doorPositions.push({ x: doorX, y: doorY, connectedRoom: connectedRooms[0] }); + } else { + // Multiple doors - space vertically + const topY = roomPosition.y + (TILE_SIZE * 2); + const bottomY = roomPosition.y + roomHeightPx - (TILE_SIZE * 3); + const spacing = (bottomY - topY) / (connectedRooms.length - 1); + + connectedRooms.forEach((connectedRoom, index) => { + const doorY = topY + (spacing * index); + doorPositions.push({ x: doorX, y: doorY, connectedRoom }); + }); + } + + return doorPositions; +} + +/** + * Calculate all door positions for a room + * This is the main entry point for door placement + */ +function calculateDoorPositionsForRoom(roomId, roomPosition, roomDimensions, connections, allPositions, allDimensions, gameScenario) { + const doorPositions = []; + + ['north', 'south', 'east', 'west'].forEach(direction => { + const connected = connections[direction]; + if (!connected) return; + + const connectedRooms = Array.isArray(connected) ? connected : [connected]; + + let positions; + if (connectedRooms.length === 1) { + // Single connection + const connectedRoom = connectedRooms[0]; + let doorPos; + + switch (direction) { + case 'north': + doorPos = placeNorthDoorSingle(roomId, roomPosition, roomDimensions, connectedRoom, gameScenario, allPositions, allDimensions); + break; + case 'south': + doorPos = placeSouthDoorSingle(roomId, roomPosition, roomDimensions, connectedRoom, gameScenario, allPositions, allDimensions); + break; + case 'east': + doorPos = placeEastDoorSingle(roomId, roomPosition, roomDimensions, connectedRoom); + break; + case 'west': + doorPos = placeWestDoorSingle(roomId, roomPosition, roomDimensions, connectedRoom); + break; + } + + if (doorPos) { + doorPositions.push({ ...doorPos, direction }); + } + } else { + // Multiple connections + switch (direction) { + case 'north': + positions = placeNorthDoorsMultiple(roomId, roomPosition, roomDimensions, connectedRooms); + break; + case 'south': + positions = placeSouthDoorsMultiple(roomId, roomPosition, roomDimensions, connectedRooms); + break; + case 'east': + positions = placeEastDoorsMultiple(roomId, roomPosition, roomDimensions, connectedRooms); + break; + case 'west': + positions = placeWestDoorsMultiple(roomId, roomPosition, roomDimensions, connectedRooms); + break; + } + + if (positions) { + positions.forEach(pos => doorPositions.push({ ...pos, direction })); + } + } + }); + + return doorPositions; +} + // Initialize door system export function initializeDoors(gameInstance, roomsRef) { gameRef = gameInstance; @@ -51,267 +368,148 @@ export function createDoorSpritesForRoom(roomId, position) { console.log(`No connections found for room ${roomId}`); return []; } - - // console.log(`Creating door sprites for room ${roomId}:`, roomData.connections); - + const doorSprites = []; - const connections = roomData.connections; - - // Get room dimensions for door positioning - const map = gameRef.cache.tilemap.get(roomData.type); - let roomWidth = 800, roomHeight = 600; // fallback - - if (map) { - if (map.json) { - roomWidth = map.json.width * TILE_SIZE; - roomHeight = map.json.height * TILE_SIZE; - } else if (map.data) { - roomWidth = map.data.width * TILE_SIZE; - roomHeight = map.data.height * TILE_SIZE; + + // Get room dimensions from global cache (set by calculateRoomPositions) + const roomDimensions = window.roomDimensions?.[roomId]; + if (!roomDimensions) { + console.error(`Room dimensions not found for ${roomId}. Did calculateRoomPositions run?`); + return []; + } + + console.log(`Creating doors for ${roomId} at (${position.x}, ${position.y}), dimensions: ${roomDimensions.widthTiles}×${roomDimensions.heightTiles} tiles`); + + // Get all positions and dimensions for door alignment + const allPositions = window.roomPositions || {}; + const allDimensions = window.roomDimensions || {}; + + // Calculate door positions using the new system + const doorPositions = calculateDoorPositionsForRoom( + roomId, + position, + roomDimensions, + roomData.connections, + allPositions, + allDimensions, + gameScenario + ); + + console.log(`Calculated ${doorPositions.length} door positions for ${roomId}`); + + // Create door sprites for each calculated position + doorPositions.forEach(doorInfo => { + const { x: doorX, y: doorY, direction, connectedRoom } = doorInfo; + + // Set door size based on direction + let doorWidth = TILE_SIZE; + let doorHeight = TILE_SIZE * 2; + + if (direction === 'east' || direction === 'west') { + doorWidth = TILE_SIZE * 2; + doorHeight = TILE_SIZE; } - } - - console.log(`Room ${roomId} dimensions: ${roomWidth}x${roomHeight}, position: (${position.x}, ${position.y})`); - - // Create door sprites for each connection direction - Object.entries(connections).forEach(([direction, connectedRooms]) => { - const roomList = Array.isArray(connectedRooms) ? connectedRooms : [connectedRooms]; - - roomList.forEach((connectedRoom, index) => { - // Calculate door position based on direction - let doorX, doorY; - let doorWidth = TILE_SIZE, doorHeight = TILE_SIZE * 2; - - switch (direction) { - case 'north': - // Door at top of room, 1.5 tiles in from sides - if (roomList.length === 1) { - // Single connection - check the connecting room's connections to determine position - const connectingRoom = roomList[0]; - const connectingRoomConnections = window.gameScenario.rooms[connectingRoom]?.connections?.south; - - if (Array.isArray(connectingRoomConnections) && connectingRoomConnections.length > 1) { - // The connecting room has multiple south doors, find which one connects to this room - const doorIndex = connectingRoomConnections.indexOf(roomId); - if (doorIndex >= 0) { - // When the connecting room has multiple doors, position this door to match - // If this room is at index 0 (left), position door on the right (southeast) - // If this room is at index 1 (right), position door on the left (southwest) - if (doorIndex === 0) { - // This room is on the left, so door should be on the right - doorX = position.x + roomWidth - TILE_SIZE * 1.5; - // console.log(`North door positioning for ${roomId}: left room (index 0), door on right (southeast), doorX=${doorX}`); - } else { - // This room is on the right, so door should be on the left - doorX = position.x + TILE_SIZE * 1.5; - // console.log(`North door positioning for ${roomId}: right room (index ${doorIndex}), door on left (southwest), doorX=${doorX}`); - } - } else { - // Fallback to left positioning - doorX = position.x + TILE_SIZE * 1.5; - // console.log(`North door positioning for ${roomId}: fallback to left, doorX=${doorX}`); - } - } else { - // Single door - use left positioning - doorX = position.x + TILE_SIZE * 1.5; - // console.log(`North door positioning for ${roomId}: single connection to ${connectingRoom}, doorX=${doorX}`); - } - } else { - // Multiple connections - use 1.5 tile spacing from edges - const availableWidth = roomWidth - (TILE_SIZE * 1.5 * 2); // Subtract edge spacing - const doorSpacing = availableWidth / (roomList.length - 1); // Space between doors - doorX = position.x + TILE_SIZE * 1.5 + (doorSpacing * index); // Start at 1.5 tiles from edge - } - doorY = position.y + TILE_SIZE; // 1 tile from top - // console.log(`North door Y position: ${doorY} (position.y=${position.y}, TILE_SIZE=${TILE_SIZE})`); - break; - case 'south': - // Door at bottom of room, 1.5 tiles in from sides - if (roomList.length === 1) { - // Single connection - check if the connecting room has multiple doors - const connectingRoom = roomList[0]; - const connectingRoomConnections = window.gameScenario.rooms[connectingRoom]?.connections?.north; - if (Array.isArray(connectingRoomConnections) && connectingRoomConnections.length > 1) { - // The connecting room has multiple north doors, find which one connects to this room - const doorIndex = connectingRoomConnections.indexOf(roomId); - if (doorIndex >= 0) { - // When the connecting room has multiple doors, position this door to match - // If this room is at index 0 (left), position door on the right (southeast) - // If this room is at index 1 (right), position door on the left (southwest) - if (doorIndex === 0) { - // This room is on the left, so door should be on the right - doorX = position.x + roomWidth - TILE_SIZE * 1.5; - // console.log(`South door positioning for ${roomId}: left room (index 0), door on right (southeast), doorX=${doorX}`); - } else { - // This room is on the right, so door should be on the left - doorX = position.x + TILE_SIZE * 1.5; - // console.log(`South door positioning for ${roomId}: right room (index ${doorIndex}), door on left (southwest), doorX=${doorX}`); - } - } else { - // Fallback to left positioning - doorX = position.x + TILE_SIZE * 1.5; - // console.log(`South door positioning for ${roomId}: fallback to left, doorX=${doorX}`); - } - } else { - // Single door - use left positioning - doorX = position.x + TILE_SIZE * 1.5; - // console.log(`South door positioning for ${roomId}: single connection to ${connectingRoom}, doorX=${doorX}`); - } - } else { - // Multiple connections - use 1.5 tile spacing from edges - const availableWidth = roomWidth - (TILE_SIZE * 1.5 * 2); // Subtract edge spacing - const doorSpacing = availableWidth / (roomList.length - 1); // Space between doors - doorX = position.x + TILE_SIZE * 1.5 + (doorSpacing * index); // Start at 1.5 tiles from edge - } - doorY = position.y + roomHeight - TILE_SIZE; // 1 tile from bottom - // replace the bottom most tile with a copy of the tile above - - - break; - case 'east': - // Door at right side of room, 1 tile in from top/bottom - doorX = position.x + roomWidth - TILE_SIZE; // 1 tile from right - doorY = position.y + roomHeight / 2; // Center of room - doorWidth = TILE_SIZE * 2; - doorHeight = TILE_SIZE; - break; - case 'west': - // Door at left side of room, 1 tile in from top/bottom - doorX = position.x + TILE_SIZE; // 1 tile from left - doorY = position.y + roomHeight / 2; // Center of room - doorWidth = TILE_SIZE * 2; - doorHeight = TILE_SIZE; - break; - default: - return; // Skip unknown directions - } - - // Create door sprite - // console.log(`Creating door sprite at (${doorX}, ${doorY}) for ${roomId} -> ${connectedRoom}`); - - // Create a colored rectangle as a fallback if door texture fails - let doorSprite; - try { - doorSprite = gameRef.add.sprite(doorX, doorY, 'door_32'); - } catch (error) { - console.warn(`Failed to create door sprite with 'door_32' texture, creating colored rectangle instead:`, error); - // Create a colored rectangle as fallback - const graphics = gameRef.add.graphics(); - graphics.fillStyle(0xff0000, 1); // Red color - graphics.fillRect(-TILE_SIZE/2, -TILE_SIZE, TILE_SIZE, TILE_SIZE * 2); - graphics.setPosition(doorX, doorY); - doorSprite = graphics; - } - doorSprite.setOrigin(0.5, 0.5); - doorSprite.setDepth(doorY + 0.45); // World Y + door layer offset - doorSprite.setAlpha(1); // Visible by default - doorSprite.setVisible(true); // Ensure visibility - - // console.log(`Door sprite created:`, { - // x: doorSprite.x, - // y: doorSprite.y, - // visible: doorSprite.visible, - // alpha: doorSprite.alpha, - // depth: doorSprite.depth, - // texture: doorSprite.texture?.key, - // width: doorSprite.width, - // height: doorSprite.height, - // displayWidth: doorSprite.displayWidth, - // displayHeight: doorSprite.displayHeight - // }); - // console.log(`Door depth: ${doorSprite.depth} (roomDepth: ${doorY}, between tiles and sprites)`); - - // Get lock properties from either the door object or the destination room - // First check if this door has explicit lock properties in the scenario - const doorDefinition = roomData.doors?.find(d => - d.connectedRoom === connectedRoom && d.direction === direction - ); - - // Lock properties can come from the door definition or the connected room - const lockProps = doorDefinition || {}; - const connectedRoomData = gameScenario.rooms[connectedRoom]; - - // Check for both keyPins (camelCase) and key_pins (snake_case) in the room data - const keyPinsArray = lockProps.keyPins || lockProps.key_pins || - connectedRoomData?.keyPins || connectedRoomData?.key_pins; - - // DEBUG: Log what we're finding - if (connectedRoomData?.locked) { - console.log(`🔍 Door keyPins lookup for ${connectedRoom}:`, { - connectedRoomData_keyPins: connectedRoomData?.keyPins, - connectedRoomData_key_pins: connectedRoomData?.key_pins, - finalKeyPinsArray: keyPinsArray, - locked: connectedRoomData?.locked, - lockType: connectedRoomData?.lockType, - requires: connectedRoomData?.requires - }); - } - - // Set up door properties - doorSprite.doorProperties = { - roomId: roomId, - connectedRoom: connectedRoom, - direction: direction, - worldX: doorX, - worldY: doorY, - open: false, - locked: lockProps.locked !== undefined ? lockProps.locked : (connectedRoomData?.locked || false), - lockType: lockProps.lockType || connectedRoomData?.lockType || null, - requires: lockProps.requires || connectedRoomData?.requires || null, - keyPins: keyPinsArray, // Include keyPins from scenario (supports both cases) - difficulty: lockProps.difficulty || connectedRoomData?.difficulty // Include difficulty from scenario - }; - - // Debug door properties - console.log(`🚪 Door properties set for ${roomId} -> ${connectedRoom}:`, { - locked: doorSprite.doorProperties.locked, - lockType: doorSprite.doorProperties.lockType, - requires: doorSprite.doorProperties.requires, - keyPins: doorSprite.doorProperties.keyPins, - difficulty: doorSprite.doorProperties.difficulty + + console.log(`Creating door sprite at (${doorX}, ${doorY}) for ${roomId} -> ${connectedRoom} (${direction})`); + + // Create a colored rectangle as a fallback if door texture fails + let doorSprite; + try { + doorSprite = gameRef.add.sprite(doorX, doorY, 'door_32'); + } catch (error) { + console.warn(`Failed to create door sprite with 'door_32' texture, creating colored rectangle instead:`, error); + // Create a colored rectangle as fallback + const graphics = gameRef.add.graphics(); + graphics.fillStyle(0xff0000, 1); // Red color + graphics.fillRect(-TILE_SIZE/2, -TILE_SIZE, TILE_SIZE, TILE_SIZE * 2); + graphics.setPosition(doorX, doorY); + doorSprite = graphics; + } + doorSprite.setOrigin(0.5, 0.5); + doorSprite.setDepth(doorY + 0.45); // World Y + door layer offset + doorSprite.setAlpha(1); // Visible by default + doorSprite.setVisible(true); // Ensure visibility + + // Get lock properties from either the door object or the destination room + // First check if this door has explicit lock properties in the scenario + const doorDefinition = roomData.doors?.find(d => + d.connectedRoom === connectedRoom && d.direction === direction + ); + + // Lock properties can come from the door definition or the connected room + const lockProps = doorDefinition || {}; + const connectedRoomData = gameScenario.rooms[connectedRoom]; + + // Check for both keyPins (camelCase) and key_pins (snake_case) in the room data + const keyPinsArray = lockProps.keyPins || lockProps.key_pins || + connectedRoomData?.keyPins || connectedRoomData?.key_pins; + + // DEBUG: Log what we're finding + if (connectedRoomData?.locked) { + console.log(`🔍 Door keyPins lookup for ${connectedRoom}:`, { + connectedRoomData_keyPins: connectedRoomData?.keyPins, + connectedRoomData_key_pins: connectedRoomData?.key_pins, + finalKeyPinsArray: keyPinsArray, + locked: connectedRoomData?.locked, + lockType: connectedRoomData?.lockType, + requires: connectedRoomData?.requires }); - - // Set up door info for transition detection - doorSprite.doorInfo = { - roomId: roomId, - connectedRoom: connectedRoom, - direction: direction - }; - - // Set up collision - gameRef.physics.add.existing(doorSprite); - doorSprite.body.setSize(doorWidth, doorHeight); - doorSprite.body.setImmovable(true); - - // Add collision with player - if (window.player && window.player.body) { - gameRef.physics.add.collider(window.player, doorSprite); - } - - // Set up interaction zone - const zone = gameRef.add.zone(doorX, doorY, doorWidth, doorHeight); - zone.setInteractive({ useHandCursor: true }); - zone.on('pointerdown', () => handleDoorInteraction(doorSprite)); - - doorSprite.interactionZone = zone; - doorSprites.push(doorSprite); - - // console.log(`Created door sprite for ${roomId} -> ${connectedRoom} (${direction}) at (${doorX}, ${doorY})`); + } + + // Set up door properties + doorSprite.doorProperties = { + roomId: roomId, + connectedRoom: connectedRoom, + direction: direction, + worldX: doorX, + worldY: doorY, + open: false, + locked: lockProps.locked !== undefined ? lockProps.locked : (connectedRoomData?.locked || false), + lockType: lockProps.lockType || connectedRoomData?.lockType || null, + requires: lockProps.requires || connectedRoomData?.requires || null, + keyPins: keyPinsArray, // Include keyPins from scenario (supports both cases) + difficulty: lockProps.difficulty || connectedRoomData?.difficulty // Include difficulty from scenario + }; + + // Debug door properties + console.log(`🚪 Door properties set for ${roomId} -> ${connectedRoom}:`, { + locked: doorSprite.doorProperties.locked, + lockType: doorSprite.doorProperties.lockType, + requires: doorSprite.doorProperties.requires, + keyPins: doorSprite.doorProperties.keyPins, + difficulty: doorSprite.doorProperties.difficulty }); + + // Set up door info for transition detection + doorSprite.doorInfo = { + roomId: roomId, + connectedRoom: connectedRoom, + direction: direction + }; + + // Set up collision + gameRef.physics.add.existing(doorSprite); + doorSprite.body.setSize(doorWidth, doorHeight); + doorSprite.body.setImmovable(true); + + // Add collision with player + if (window.player && window.player.body) { + gameRef.physics.add.collider(window.player, doorSprite); + } + + // Set up interaction zone + const zone = gameRef.add.zone(doorX, doorY, doorWidth, doorHeight); + zone.setInteractive({ useHandCursor: true }); + zone.on('pointerdown', () => handleDoorInteraction(doorSprite)); + + doorSprite.interactionZone = zone; + doorSprites.push(doorSprite); + + console.log(`Created door sprite for ${roomId} -> ${connectedRoom} (${direction}) at (${doorX}, ${doorY})`); }); - - // console.log(`Created ${doorSprites.length} door sprites for room ${roomId}`); - - // Log camera position for debugging - if (gameRef.cameras && gameRef.cameras.main) { - console.log(`Camera position:`, { - x: gameRef.cameras.main.scrollX, - y: gameRef.cameras.main.scrollY, - width: gameRef.cameras.main.width, - height: gameRef.cameras.main.height - }); - } - + + console.log(`Created ${doorSprites.length} door sprites for room ${roomId}`); + return doorSprites; } diff --git a/js/utils/constants.js b/js/utils/constants.js index d9e61fb..1e6d25f 100644 --- a/js/utils/constants.js +++ b/js/utils/constants.js @@ -2,6 +2,17 @@ export const TILE_SIZE = 32; export const DOOR_ALIGN_OVERLAP = 32 * 3; export const GRID_SIZE = 32; + +// Grid unit system constants for room layout +// Grid units are the base stacking size for rooms +export const GRID_UNIT_WIDTH_TILES = 5; // 5 tiles wide +export const GRID_UNIT_HEIGHT_TILES = 4; // 4 tiles tall (stacking area) +export const VISUAL_TOP_TILES = 2; // Top 2 rows are visual wall overlay + +// Calculated grid unit sizes in pixels +export const GRID_UNIT_WIDTH_PX = GRID_UNIT_WIDTH_TILES * TILE_SIZE; // 160px +export const GRID_UNIT_HEIGHT_PX = GRID_UNIT_HEIGHT_TILES * TILE_SIZE; // 128px + export const MOVEMENT_SPEED = 150; export const RUN_SPEED_MULTIPLIER = 1.5; // Speed multiplier when holding shift export const RUN_ANIMATION_MULTIPLIER = 1.5; // Animation speed multiplier when holding shift