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
This commit is contained in:
Z. Cliffe Schreuders
2025-11-16 08:45:06 +00:00
parent 57d9b8f5d8
commit 2e62a4e62b
3 changed files with 894 additions and 388 deletions

View File

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

View File

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

View File

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