Add Room Management and Collision Systems: Introduce a new room management system with simplified depth layering for object rendering. Implement collision management and door systems for improved player interactions. Refactor existing room logic to enhance modularity and maintainability, separating concerns into dedicated modules for doors, collisions, and object physics. Update relevant files to ensure seamless integration with the game environment.

This commit is contained in:
Z. Cliffe Schreuders
2025-10-10 23:27:47 +01:00
parent ebef1bcd12
commit 7c23395b05
8 changed files with 3808 additions and 1777 deletions

File diff suppressed because it is too large Load Diff

1519
js/core/rooms_new.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -112,6 +112,22 @@ export class LockpickingMinigamePhaser extends MinigameScene {
}
}
// Method to get the lock's pin configuration for key generation
getLockPinConfiguration() {
if (!this.pins || this.pins.length === 0) {
return null;
}
return {
pinCount: this.pinCount,
pinHeights: this.pins.map(pin => pin.originalHeight),
pinLengths: this.pins.map(pin => ({
keyPinLength: pin.keyPinLength,
driverPinLength: pin.driverPinLength
}))
};
}
loadLockConfiguration() {
// Load lock configuration from global storage
const config = window.lockConfigurations[this.lockId];
@@ -1691,7 +1707,15 @@ export class LockpickingMinigamePhaser extends MinigameScene {
this.updateKeyPosition(0);
// Show key selection again
if (this.keySelectionMode) {
this.createKeysForChallenge('correct_key');
// For main game, go back to original key selection interface
// For challenge mode (locksmith-forge.html), use the training interface
if (this.params?.lockable?.id === 'progressive-challenge') {
// This is the locksmith-forge.html challenge mode
this.createKeysForChallenge('correct_key');
} else {
// This is the main game - go back to key selection
this.startWithKeySelection();
}
}
}, 2000); // Longer delay to show the red flash
}
@@ -1704,11 +1728,32 @@ export class LockpickingMinigamePhaser extends MinigameScene {
console.log('Snapping pins to exact positions based on key cuts for shear line alignment');
// Ensure key data matches lock pin count
if (keyDataToUse.cuts.length !== this.pinCount) {
console.warn(`Key has ${keyDataToUse.cuts.length} cuts but lock has ${this.pinCount} pins. Adjusting key data.`);
// Truncate or pad cuts to match pin count
if (keyDataToUse.cuts.length > this.pinCount) {
keyDataToUse.cuts = keyDataToUse.cuts.slice(0, this.pinCount);
} else {
// Pad with default cuts if key has fewer cuts than lock has pins
while (keyDataToUse.cuts.length < this.pinCount) {
keyDataToUse.cuts.push(40); // Default cut depth
}
}
}
// Set each pin to the exact final position based on key cut dimensions
keyDataToUse.cuts.forEach((cutDepth, index) => {
if (index >= this.pinCount) return;
if (index >= this.pinCount) {
console.warn(`Key has ${keyDataToUse.cuts.length} cuts but lock only has ${this.pinCount} pins. Skipping cut ${index}.`);
return;
}
const pin = this.pins[index];
if (!pin) {
console.error(`Pin at index ${index} is undefined. Available pins: ${this.pins.length}`);
return;
}
// Calculate the exact position where the pin should rest on the key cut
// The cut depth represents how deep the cut is from the blade top
@@ -1722,6 +1767,11 @@ export class LockpickingMinigamePhaser extends MinigameScene {
const cutSurfaceY = keyBladeBaseY + cutDepth;
// Calculate where the pin bottom should be to rest on the cut surface
// Add safety check for undefined properties
if (!pin.driverPinLength || !pin.keyPinLength) {
console.warn(`Pin ${pin.index} missing length properties:`, pin);
return; // Skip this pin if properties are missing
}
const pinRestY = 200 - 50 + pin.driverPinLength + pin.keyPinLength; // Pin rest position
const targetKeyPinBottom = cutSurfaceY;
@@ -2632,6 +2682,11 @@ export class LockpickingMinigamePhaser extends MinigameScene {
const pinWorldY = 200;
// Calculate pin's current position (including any existing movement)
// Add safety check for undefined properties
if (!pin.driverPinLength || !pin.keyPinLength) {
console.warn(`Pin ${pinIndex} missing length properties in checkHookCollisions:`, pin);
return; // Skip this pin if properties are missing
}
const pinCurrentY = pinWorldY - 50 + pin.driverPinLength + pin.keyPinLength - pin.currentHeight;
const keyPinTop = pinCurrentY - pin.keyPinLength;
const keyPinBottom = pinCurrentY;
@@ -2714,6 +2769,11 @@ export class LockpickingMinigamePhaser extends MinigameScene {
pin.keyPin.fillStyle(0xdd3333);
// Calculate new position based on currentHeight
// Add safety check for undefined properties
if (!pin.driverPinLength || !pin.keyPinLength) {
console.warn(`Pin ${pin.index} missing length properties in updatePinVisuals:`, pin);
return; // Skip this pin if properties are missing
}
const newKeyPinY = -50 + pin.driverPinLength - pin.currentHeight;
const keyPinTopY = newKeyPinY;
const keyPinBottomY = newKeyPinY + pin.keyPinLength;
@@ -2773,13 +2833,21 @@ export class LockpickingMinigamePhaser extends MinigameScene {
// Try to load saved pin heights for this lock
const savedPinHeights = this.loadLockConfiguration();
// Check if predefined pin heights were passed
const predefinedPinHeights = this.params?.predefinedPinHeights;
for (let i = 0; i < this.pinCount; i++) {
const pinX = 100 + margin + i * pinSpacing;
const pinY = 200;
// Use saved pin heights if available, otherwise generate random ones
// Use predefined pin heights if available, otherwise use saved or generate random ones
let keyPinLength, driverPinLength;
if (savedPinHeights && savedPinHeights[i] !== undefined) {
if (predefinedPinHeights && predefinedPinHeights[i] !== undefined) {
// Use predefined configuration
keyPinLength = predefinedPinHeights[i];
driverPinLength = 75 - keyPinLength; // Total height is 75
console.log(`Using predefined pin height for pin ${i}: ${keyPinLength}`);
} else if (savedPinHeights && savedPinHeights[i] !== undefined) {
// Use saved configuration
keyPinLength = savedPinHeights[i];
driverPinLength = 75 - keyPinLength; // Total height is 75
@@ -2807,6 +2875,13 @@ export class LockpickingMinigamePhaser extends MinigameScene {
spring: null
};
// Ensure pin properties are valid
if (!pin.keyPinLength || !pin.driverPinLength) {
console.error(`Pin ${i} created with invalid lengths:`, pin);
pin.keyPinLength = pin.keyPinLength || 30; // Default fallback
pin.driverPinLength = pin.driverPinLength || 45; // Default fallback
}
// Create pin container
pin.container = this.scene.add.container(pinX, pinY);

601
js/systems/collision.js Normal file
View File

@@ -0,0 +1,601 @@
/**
* COLLISION MANAGEMENT SYSTEM
* ===========================
*
* Handles static collision geometry, tile-based collision, and wall management.
* Separated from rooms.js for better modularity and maintainability.
*/
import { TILE_SIZE } from '../utils/constants.js';
import { getOppositeDirection } from './doors.js';
let gameRef = null;
let rooms = null;
// Initialize collision system
export function initializeCollision(gameInstance, roomsRef) {
gameRef = gameInstance;
rooms = roomsRef;
}
// Function to create thin collision boxes for wall tiles
export function createWallCollisionBoxes(wallLayer, roomId, position) {
console.log(`Creating wall collision boxes for room ${roomId}`);
// Get room dimensions from the map
const map = rooms[roomId].map;
const roomWidth = map.widthInPixels;
const roomHeight = map.heightInPixels;
console.log(`Room ${roomId} dimensions: ${roomWidth}x${roomHeight} at position (${position.x}, ${position.y})`);
const collisionBoxes = [];
// Get all wall tiles from the layer
const wallTiles = wallLayer.getTilesWithin(0, 0, map.width, map.height, { isNotEmpty: true });
wallTiles.forEach(tile => {
const tileX = tile.x;
const tileY = tile.y;
const worldX = position.x + (tileX * TILE_SIZE);
const worldY = position.y + (tileY * TILE_SIZE);
// Create collision boxes for all applicable edges (not just one)
const tileCollisionBoxes = [];
// North wall (top 2 rows) - collision on south edge
if (tileY < 2) {
const collisionBox = gameRef.add.rectangle(
worldX + TILE_SIZE / 2,
worldY + TILE_SIZE - 4, // 4px from south edge
TILE_SIZE,
8, // Thicker collision box
0x000000,
0 // Invisible
);
tileCollisionBoxes.push(collisionBox);
}
// South wall (bottom row) - collision on south edge
if (tileY === map.height - 1) {
const collisionBox = gameRef.add.rectangle(
worldX + TILE_SIZE / 2,
worldY + TILE_SIZE - 4, // 4px from south edge
TILE_SIZE,
8, // Thicker collision box
0x000000,
0 // Invisible
);
tileCollisionBoxes.push(collisionBox);
}
// West wall (left column) - collision on east edge
if (tileX === 0) {
const collisionBox = gameRef.add.rectangle(
worldX + TILE_SIZE - 4, // 4px from east edge
worldY + TILE_SIZE / 2,
8, // Thicker collision box
TILE_SIZE,
0x000000,
0 // Invisible
);
tileCollisionBoxes.push(collisionBox);
}
// East wall (right column) - collision on west edge
if (tileX === map.width - 1) {
const collisionBox = gameRef.add.rectangle(
worldX + 4, // 4px from west edge
worldY + TILE_SIZE / 2,
8, // Thicker collision box
TILE_SIZE,
0x000000,
0 // Invisible
);
tileCollisionBoxes.push(collisionBox);
}
// Set up all collision boxes for this tile
tileCollisionBoxes.forEach(collisionBox => {
collisionBox.setVisible(false);
gameRef.physics.add.existing(collisionBox, true);
// Wait for the next frame to ensure body is fully initialized
gameRef.time.delayedCall(0, () => {
if (collisionBox.body) {
// Use direct property assignment (fallback method)
collisionBox.body.immovable = true;
}
});
collisionBoxes.push(collisionBox);
});
});
console.log(`Created ${collisionBoxes.length} wall collision boxes for room ${roomId}`);
// Add collision with player for all collision boxes
const player = window.player;
if (player && player.body) {
collisionBoxes.forEach(collisionBox => {
gameRef.physics.add.collider(player, collisionBox);
});
console.log(`Added ${collisionBoxes.length} wall collision boxes for room ${roomId}`);
} else {
console.warn(`Player not ready for room ${roomId}, storing ${collisionBoxes.length} collision boxes for later`);
if (!rooms[roomId].pendingWallCollisionBoxes) {
rooms[roomId].pendingWallCollisionBoxes = [];
}
rooms[roomId].pendingWallCollisionBoxes.push(...collisionBoxes);
}
// Store collision boxes in room for cleanup
if (!rooms[roomId].wallCollisionBoxes) {
rooms[roomId].wallCollisionBoxes = [];
}
rooms[roomId].wallCollisionBoxes.push(...collisionBoxes);
}
// Function to remove wall tiles under doors
export function removeTilesUnderDoor(wallLayer, roomId, position) {
console.log(`Removing wall tiles under doors in room ${roomId}`);
// Remove wall tiles under doors using the same positioning logic as door sprites
const gameScenario = window.gameScenario;
const roomData = gameScenario.rooms[roomId];
if (!roomData || !roomData.connections) {
console.log(`No connections found for room ${roomId}, skipping wall tile removal`);
return;
}
// Get room dimensions for door positioning (same as door sprite creation)
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;
}
}
const connections = roomData.connections;
// Process each connection direction
Object.entries(connections).forEach(([direction, connectedRooms]) => {
const roomList = Array.isArray(connectedRooms) ? connectedRooms : [connectedRooms];
roomList.forEach((connectedRoom, index) => {
// Calculate door position using the same logic as door sprite creation
let doorX, doorY;
let doorWidth = TILE_SIZE, doorHeight = TILE_SIZE * 2;
switch (direction) {
case 'north':
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;
} else {
// This room is on the right, so door should be on the left
doorX = position.x + TILE_SIZE * 1.5;
}
} else {
// Fallback to left positioning
doorX = position.x + TILE_SIZE * 1.5;
}
} else {
// Single door - use left positioning
doorX = position.x + TILE_SIZE * 1.5;
}
} 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;
break;
case 'south':
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;
} else {
// This room is on the right, so door should be on the left
doorX = position.x + TILE_SIZE * 1.5;
}
} else {
// Fallback to left positioning
doorX = position.x + TILE_SIZE * 1.5;
}
} else {
// Single door - use left positioning
doorX = position.x + TILE_SIZE * 1.5;
}
} 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;
break;
case 'east':
doorX = position.x + roomWidth - TILE_SIZE;
doorY = position.y + roomHeight / 2;
doorWidth = TILE_SIZE * 2;
doorHeight = TILE_SIZE;
break;
case 'west':
doorX = position.x + TILE_SIZE;
doorY = position.y + roomHeight / 2;
doorWidth = TILE_SIZE * 2;
doorHeight = TILE_SIZE;
break;
default:
return;
}
// Use Phaser's getTilesWithin to get tiles that overlap with the door area
const doorBounds = {
x: doorX - (doorWidth / 2), // Door sprite origin is center, so adjust bounds
y: doorY - (doorHeight / 2),
width: doorWidth,
height: doorHeight
};
// Convert door bounds to tilemap coordinates (relative to the layer)
const doorBoundsInTilemap = {
x: doorBounds.x - wallLayer.x,
y: doorBounds.y - wallLayer.y,
width: doorBounds.width,
height: doorBounds.height
};
console.log(`Removing wall tiles for ${roomId} -> ${connectedRoom} (${direction}): door at (${doorX}, ${doorY}), world bounds:`, doorBounds, `tilemap bounds:`, doorBoundsInTilemap);
console.log(`Wall layer info: x=${wallLayer.x}, y=${wallLayer.y}, width=${wallLayer.width}, height=${wallLayer.height}`);
// Try a different approach - convert to tile coordinates first
const doorTileX = Math.floor(doorBoundsInTilemap.x / TILE_SIZE);
const doorTileY = Math.floor(doorBoundsInTilemap.y / TILE_SIZE);
const doorTilesWide = Math.ceil(doorBoundsInTilemap.width / TILE_SIZE);
const doorTilesHigh = Math.ceil(doorBoundsInTilemap.height / TILE_SIZE);
console.log(`Door tile coordinates: (${doorTileX}, ${doorTileY}) covering ${doorTilesWide}x${doorTilesHigh} tiles`);
// Check what tiles exist in the door area manually
let foundTiles = [];
for (let x = 0; x < doorTilesWide; x++) {
for (let y = 0; y < doorTilesHigh; y++) {
const tileX = doorTileX + x;
const tileY = doorTileY + y;
const tile = wallLayer.getTileAt(tileX, tileY);
if (tile && tile.index !== -1) {
foundTiles.push({x: tileX, y: tileY, tile: tile});
console.log(`Found wall tile at (${tileX}, ${tileY}) with index ${tile.index}`);
}
}
}
console.log(`Manually found ${foundTiles.length} wall tiles in door area`);
// Get all tiles within the door bounds (using tilemap coordinates)
const overlappingTiles = wallLayer.getTilesWithin(
doorBoundsInTilemap.x,
doorBoundsInTilemap.y,
doorBoundsInTilemap.width,
doorBoundsInTilemap.height
);
console.log(`getTilesWithin found ${overlappingTiles.length} tiles overlapping with door area`);
// Use the manually found tiles if getTilesWithin didn't work
const tilesToRemove = foundTiles.length > 0 ? foundTiles : overlappingTiles;
// Remove wall tiles that overlap with the door
tilesToRemove.forEach(tileData => {
const tileX = tileData.x;
const tileY = tileData.y;
// Remove the wall tile
const removedTile = wallLayer.tilemap.removeTileAt(
tileX,
tileY,
true, // replaceWithNull
true, // recalculateFaces
wallLayer // layer
);
if (removedTile) {
console.log(`Removed wall tile at (${tileX}, ${tileY}) under door ${roomId} -> ${connectedRoom}`);
}
});
// Recalculate collision after removing tiles
if (tilesToRemove.length > 0) {
console.log(`Recalculating collision for wall layer in ${roomId} after removing ${tilesToRemove.length} tiles`);
wallLayer.setCollisionByExclusion([-1]);
}
});
});
}
// Function to remove wall tiles from a specific room for a door connection
export function removeWallTilesForDoorInRoom(roomId, fromRoomId, direction, doorWorldX, doorWorldY) {
console.log(`Removing wall tiles in room ${roomId} for door from ${fromRoomId} (${direction}) at world position (${doorWorldX}, ${doorWorldY})`);
const room = rooms[roomId];
if (!room || !room.wallsLayers || room.wallsLayers.length === 0) {
console.log(`No wall layers found for room ${roomId}`);
return;
}
// Calculate the door position in the connected room
// The door should be on the opposite side of the connection
const oppositeDirection = getOppositeDirection(direction);
const roomPosition = window.roomPositions[roomId];
const roomData = window.gameScenario.rooms[roomId];
if (!roomPosition || !roomData) {
console.log(`Missing position or data for room ${roomId}`);
return;
}
// Get room dimensions
const roomWidth = roomData.width || 320;
const roomHeight = roomData.height || 288;
// Calculate door position in the connected room based on the opposite direction
let doorX, doorY, doorWidth, doorHeight;
// Calculate door position based on the room's door configuration
if (direction === 'north' || direction === 'south') {
// For north/south connections, calculate X position based on room configuration
const oppositeDirection = getOppositeDirection(direction);
const connections = roomData.connections?.[oppositeDirection];
if (Array.isArray(connections)) {
// Multiple doors - find the one that connects to fromRoomId
const doorIndex = connections.indexOf(fromRoomId);
if (doorIndex >= 0) {
const totalDoors = connections.length;
const availableWidth = roomWidth - (TILE_SIZE * 3); // 1.5 tiles from each edge
const doorSpacing = totalDoors > 1 ? availableWidth / (totalDoors - 1) : 0;
doorX = roomPosition.x + TILE_SIZE * 1.5 + (doorIndex * doorSpacing);
} else {
doorX = roomPosition.x + roomWidth / 2; // Default to center
}
} else {
// Single door - check if the connecting room has multiple doors
const connectingRoomConnections = window.gameScenario.rooms[fromRoomId]?.connections?.[direction];
if (Array.isArray(connectingRoomConnections) && connectingRoomConnections.length > 1) {
// The connecting room has multiple 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 = roomPosition.x + roomWidth - TILE_SIZE * 1.5;
console.log(`Wall tile removal door positioning for ${roomId}: left room (index 0), door on right (southeast), calculated doorX=${doorX}`);
} else {
// This room is on the right, so door should be on the left
doorX = roomPosition.x + TILE_SIZE * 1.5;
console.log(`Wall tile removal door positioning for ${roomId}: right room (index ${doorIndex}), door on left (southwest), calculated doorX=${doorX}`);
}
} else {
// Fallback to left positioning
doorX = roomPosition.x + TILE_SIZE * 1.5;
console.log(`Wall tile removal door positioning for ${roomId}: fallback to left, calculated doorX=${doorX}`);
}
} else {
// Single door - use left positioning
doorX = roomPosition.x + TILE_SIZE * 1.5;
console.log(`Wall tile removal door positioning for ${roomId}: single connection to ${fromRoomId}, calculated doorX=${doorX}`);
}
}
if (direction === 'north') {
// Original door is north, so new door should be south
doorY = roomPosition.y + roomHeight - TILE_SIZE;
} else {
// Original door is south, so new door should be north
doorY = roomPosition.y + TILE_SIZE;
}
doorWidth = TILE_SIZE * 2;
doorHeight = TILE_SIZE;
} else if (direction === 'east' || direction === 'west') {
// For east/west connections, calculate Y position based on room configuration
doorY = roomPosition.y + roomHeight / 2; // Center of room
if (direction === 'east') {
// Original door is east, so new door should be west
doorX = roomPosition.x + TILE_SIZE;
} else {
// Original door is west, so new door should be east
doorX = roomPosition.x + roomWidth - TILE_SIZE;
}
doorWidth = TILE_SIZE;
doorHeight = TILE_SIZE * 2;
} else {
console.log(`Unknown direction: ${direction}`);
return;
}
// For debugging: Calculate what the door position should be based on room dimensions
const expectedSouthDoorY = roomPosition.y + roomHeight - TILE_SIZE;
const expectedNorthDoorY = roomPosition.y + TILE_SIZE;
console.log(`Expected door positions for ${roomId}: north=${expectedNorthDoorY}, south=${expectedSouthDoorY}`);
// Debug: Log the room position and calculated door position
console.log(`Room ${roomId} position: (${roomPosition.x}, ${roomPosition.y}), dimensions: ${roomWidth}x${roomHeight}`);
console.log(`Original door at (${doorWorldX}, ${doorWorldY}), calculated door at (${doorX}, ${doorY})`);
console.log(`Direction: ${direction}, oppositeDirection: ${getOppositeDirection(direction)}`);
console.log(`Room connections:`, roomData.connections);
console.log(`Calculated door position in ${roomId}: (${doorX}, ${doorY}) for ${oppositeDirection} connection`);
// Remove wall tiles from all wall layers in this room
room.wallsLayers.forEach(wallLayer => {
// Calculate door bounds
// For north/south doors, the door sprite origin is at the center, but we need to adjust for the actual door position
let doorBounds;
if (oppositeDirection === 'north' || oppositeDirection === 'south') {
// For north/south doors, the door should cover the full width and be positioned at the edge
doorBounds = {
x: doorX - (doorWidth / 2),
y: doorY, // Don't subtract half height - the door is positioned at the edge
width: doorWidth,
height: doorHeight
};
} else {
// For east/west doors, use center positioning
doorBounds = {
x: doorX - (doorWidth / 2),
y: doorY - (doorHeight / 2),
width: doorWidth,
height: doorHeight
};
}
// For debugging: Show the door sprite dimensions and bounds
console.log(`Door sprite at (${doorX}, ${doorY}) with dimensions ${doorWidth}x${doorHeight}`);
console.log(`Door bounds: x=${doorBounds.x}, y=${doorBounds.y}, width=${doorBounds.width}, height=${doorBounds.height}`);
// Convert door bounds to tilemap coordinates
const doorBoundsInTilemap = {
x: doorBounds.x - wallLayer.x,
y: doorBounds.y - wallLayer.y,
width: doorBounds.width,
height: doorBounds.height
};
console.log(`Removing wall tiles in ${roomId} for ${oppositeDirection} door: world bounds:`, doorBounds, `tilemap bounds:`, doorBoundsInTilemap);
console.log(`Wall layer position: (${wallLayer.x}, ${wallLayer.y}), size: ${wallLayer.width}x${wallLayer.height}`);
console.log(`Room position: (${roomPosition.x}, ${roomPosition.y}), door position: (${doorX}, ${doorY})`);
// Convert to tile coordinates
const doorTileX = Math.floor(doorBoundsInTilemap.x / TILE_SIZE);
const doorTileY = Math.floor(doorBoundsInTilemap.y / TILE_SIZE);
const doorTilesWide = Math.ceil(doorBoundsInTilemap.width / TILE_SIZE);
const doorTilesHigh = Math.ceil(doorBoundsInTilemap.height / TILE_SIZE);
console.log(`Expected tile Y: ${Math.floor((doorY - roomPosition.y) / TILE_SIZE)}, actual tile Y: ${doorTileY}`);
console.log(`Door tile coordinates in ${roomId}: (${doorTileX}, ${doorTileY}) covering ${doorTilesWide}x${doorTilesHigh} tiles`);
// Check what tiles exist in the door area manually
let foundTiles = [];
for (let x = 0; x < doorTilesWide; x++) {
for (let y = 0; y < doorTilesHigh; y++) {
const tileX = doorTileX + x;
const tileY = doorTileY + y;
const tile = wallLayer.getTileAt(tileX, tileY);
if (tile && tile.index !== -1) {
foundTiles.push({x: tileX, y: tileY, tile: tile});
console.log(`Found wall tile at (${tileX}, ${tileY}) with index ${tile.index} in ${roomId}`);
}
}
}
console.log(`Manually found ${foundTiles.length} wall tiles in door area in ${roomId}`);
// Remove wall tiles that overlap with the door
foundTiles.forEach(tileData => {
const tileX = tileData.x;
const tileY = tileData.y;
// Remove the wall tile
const removedTile = wallLayer.tilemap.removeTileAt(
tileX,
tileY,
true, // replaceWithNull
true, // recalculateFaces
wallLayer // layer
);
if (removedTile) {
console.log(`Removed wall tile at (${tileX}, ${tileY}) under door in ${roomId}`);
}
});
// Recalculate collision after removing tiles
if (foundTiles.length > 0) {
console.log(`Recalculating collision for wall layer in ${roomId} after removing ${foundTiles.length} tiles`);
wallLayer.setCollisionByExclusion([-1]);
}
});
}
// Function to remove wall tiles from all overlapping room layers at a world position
export function removeWallTilesAtWorldPosition(worldX, worldY, debugInfo = '') {
console.log(`Removing wall tiles at world position (${worldX}, ${worldY}) - ${debugInfo}`);
// Find all rooms and their wall layers that could contain this world position
Object.entries(rooms).forEach(([roomId, room]) => {
if (!room.wallsLayers || room.wallsLayers.length === 0) return;
room.wallsLayers.forEach(wallLayer => {
try {
// Convert world coordinates to tile coordinates for this layer
const tileX = Math.floor((worldX - room.position.x) / TILE_SIZE);
const tileY = Math.floor((worldY - room.position.y) / TILE_SIZE);
// Check if the tile coordinates are within the layer bounds
const wallTile = wallLayer.getTileAt(tileX, tileY);
if (wallTile && wallTile.index !== -1) {
// Remove the wall tile using the map's removeTileAt method
const removedTile = room.map.removeTileAt(
tileX,
tileY,
true, // replaceWithNull
true, // recalculateFaces
wallLayer // layer
);
if (removedTile) {
console.log(` Removed wall tile at (${tileX},${tileY}) from room ${roomId} layer ${wallLayer.name}`);
}
} else {
console.log(` No wall tile found at (${tileX},${tileY}) in room ${roomId} layer ${wallLayer.name || 'unnamed'}`);
}
} catch (error) {
console.warn(`Error removing wall tile from room ${roomId}:`, error);
}
});
});
}
// Export for global access
window.createWallCollisionBoxes = createWallCollisionBoxes;
window.removeTilesUnderDoor = removeTilesUnderDoor;
window.removeWallTilesForDoorInRoom = removeWallTilesForDoorInRoom;
window.removeWallTilesAtWorldPosition = removeWallTilesAtWorldPosition;

664
js/systems/doors.js Normal file
View File

@@ -0,0 +1,664 @@
/**
* DOOR SYSTEM
* ===========
*
* Handles door sprites, interactions, transitions, and visibility management.
* Separated from rooms.js for better modularity and maintainability.
*/
import { TILE_SIZE } from '../utils/constants.js';
import { handleUnlock, getLockRequirementsForDoor } from './interactions.js';
let gameRef = null;
let rooms = null;
// Global toggle for disabling locks during testing
window.DISABLE_LOCKS = false; // Set to true in console to bypass all lock checks
// Console helper functions for testing
window.toggleLocks = function() {
window.DISABLE_LOCKS = !window.DISABLE_LOCKS;
console.log(`Locks ${window.DISABLE_LOCKS ? 'DISABLED' : 'ENABLED'} for testing`);
return window.DISABLE_LOCKS;
};
window.disableLocks = function() {
window.DISABLE_LOCKS = true;
console.log('Locks DISABLED for testing - all doors will open without minigames');
};
window.enableLocks = function() {
window.DISABLE_LOCKS = false;
console.log('Locks ENABLED - doors will require proper unlocking');
};
// Door transition cooldown system
let lastDoorTransitionTime = 0;
const DOOR_TRANSITION_COOLDOWN = 1000; // 1 second cooldown between transitions
let lastDoorTransition = null; // Track the last door transition to prevent repeats
// Initialize door system
export function initializeDoors(gameInstance, roomsRef) {
gameRef = gameInstance;
rooms = roomsRef;
}
// Function to create door sprites based on gameScenario connections
export function createDoorSpritesForRoom(roomId, position) {
const gameScenario = window.gameScenario;
const roomData = gameScenario.rooms[roomId];
if (!roomData || !roomData.connections) {
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;
}
}
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)`);
// Set up door properties
doorSprite.doorProperties = {
roomId: roomId,
connectedRoom: connectedRoom,
direction: direction,
worldX: doorX,
worldY: doorY,
open: false,
locked: roomData.locked || false,
lockType: roomData.lockType || null,
requires: roomData.requires || null
};
// 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
});
}
return doorSprites;
}
// Function to handle door interactions
function handleDoorInteraction(doorSprite) {
const player = window.player;
if (!player) return;
const distance = Phaser.Math.Distance.Between(
player.x, player.y,
doorSprite.x, doorSprite.y
);
const DOOR_INTERACTION_RANGE = 2 * TILE_SIZE;
if (distance > DOOR_INTERACTION_RANGE) {
console.log('Door too far to interact');
return;
}
const props = doorSprite.doorProperties;
console.log(`Interacting with door: ${props.roomId} -> ${props.connectedRoom}`);
// Check if locks are disabled for testing
if (window.DISABLE_LOCKS) {
console.log('LOCKS DISABLED FOR TESTING - Opening door directly');
openDoor(doorSprite);
return;
}
if (props.locked) {
console.log(`Door is locked. Type: ${props.lockType}, Requires: ${props.requires}`);
// Use the proper lock system from interactions.js
handleUnlock(doorSprite, 'door');
} else {
openDoor(doorSprite);
}
}
// Function to unlock a door (called by interactions.js after successful unlock)
function unlockDoor(doorSprite) {
const props = doorSprite.doorProperties;
console.log(`Unlocking door: ${props.roomId} -> ${props.connectedRoom}`);
// Mark door as unlocked
props.locked = false;
// TODO: Implement unlock animation/effect
// Open the door
openDoor(doorSprite);
}
// Function to open a door
function openDoor(doorSprite) {
const props = doorSprite.doorProperties;
console.log(`Opening door: ${props.roomId} -> ${props.connectedRoom}`);
// Load the connected room if it doesn't exist
if (!rooms[props.connectedRoom]) {
console.log(`Loading room: ${props.connectedRoom}`);
// Import the loadRoom function from rooms.js
if (window.loadRoom) {
window.loadRoom(props.connectedRoom);
}
}
// Remove wall tiles from the connected room under the door position
if (window.removeWallTilesForDoorInRoom) {
window.removeWallTilesForDoorInRoom(props.connectedRoom, props.roomId, props.direction, doorSprite.x, doorSprite.y);
}
// Remove the matching door sprite from the connected room
removeMatchingDoorSprite(props.connectedRoom, props.roomId, props.direction, doorSprite.x, doorSprite.y);
// Create animated door sprite on the opposite side
createAnimatedDoorOnOppositeSide(props.connectedRoom, props.roomId, props.direction, doorSprite.x, doorSprite.y);
// Remove the door sprite
doorSprite.destroy();
if (doorSprite.interactionZone) {
doorSprite.interactionZone.destroy();
}
props.open = true;
}
// Function to remove the matching door sprite from the connected room
function removeMatchingDoorSprite(roomId, fromRoomId, direction, doorWorldX, doorWorldY) {
console.log(`Removing matching door sprite in room ${roomId} for door from ${fromRoomId} (${direction})`);
const room = rooms[roomId];
if (!room || !room.doorSprites) {
console.log(`No door sprites found for room ${roomId}`);
return;
}
// Find the door sprite that connects to the fromRoomId
const matchingDoorSprite = room.doorSprites.find(doorSprite => {
const props = doorSprite.doorProperties;
return props && props.connectedRoom === fromRoomId;
});
if (matchingDoorSprite) {
console.log(`Found matching door sprite in room ${roomId}, removing it`);
matchingDoorSprite.destroy();
if (matchingDoorSprite.interactionZone) {
matchingDoorSprite.interactionZone.destroy();
}
// Remove from the doorSprites array
const index = room.doorSprites.indexOf(matchingDoorSprite);
if (index > -1) {
room.doorSprites.splice(index, 1);
}
} else {
console.log(`No matching door sprite found in room ${roomId}`);
}
}
// Function to create animated door sprite on the opposite side
function createAnimatedDoorOnOppositeSide(roomId, fromRoomId, direction, doorWorldX, doorWorldY) {
console.log(`Creating animated door on opposite side in room ${roomId} for door from ${fromRoomId} (${direction}) at world position (${doorWorldX}, ${doorWorldY})`);
const room = rooms[roomId];
if (!room) {
console.log(`Room ${roomId} not found, cannot create animated door`);
return;
}
// Calculate the door position in the connected room
const oppositeDirection = getOppositeDirection(direction);
const roomPosition = window.roomPositions[roomId];
const roomData = window.gameScenario.rooms[roomId];
if (!roomPosition || !roomData) {
console.log(`Missing position or data for room ${roomId}`);
return;
}
// Get room dimensions from tilemap (same as door sprite creation)
const map = gameRef.cache.tilemap.get(roomData.type);
let roomWidth = 320, roomHeight = 288; // fallback (10x9 tiles at 32px)
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;
}
}
// Use the same world coordinates as the original door
let doorX = doorWorldX, doorY = doorWorldY, doorWidth, doorHeight;
// Set door dimensions based on direction
if (direction === 'north' || direction === 'south') {
doorWidth = TILE_SIZE * 2;
doorHeight = TILE_SIZE;
} else if (direction === 'east' || direction === 'west') {
doorWidth = TILE_SIZE * 2;
doorHeight = TILE_SIZE;
} else {
console.log(`Unknown direction: ${direction}`);
return;
}
// Create the animated door sprite
let animatedDoorSprite;
let doorTopSprite;
try {
// Create main door sprite
animatedDoorSprite = gameRef.add.sprite(doorX, doorY, 'door_sheet');
// Calculate the bottom of the door (where it meets the ground)
const doorBottomY = doorY + (TILE_SIZE * 2) / 2; // doorY is center, so add half height to get bottom
// Set sprite properties
animatedDoorSprite.setOrigin(0.5, 0.5);
animatedDoorSprite.setDepth(doorBottomY + 0.45); // Bottom Y + door layer offset
animatedDoorSprite.setVisible(true);
// Play the opening animation
animatedDoorSprite.play('door_open');
// Create door top sprite (6th frame) at high z-index
doorTopSprite = gameRef.add.sprite(doorX, doorY, 'door_sheet');
doorTopSprite.setOrigin(0.5, 0.5);
doorTopSprite.setDepth(doorBottomY + 0.55); // Bottom Y + door top layer offset
doorTopSprite.setVisible(true);
doorTopSprite.play('door_top');
// Store references to the animated doors in the room
if (!room.animatedDoors) {
room.animatedDoors = [];
}
room.animatedDoors.push(animatedDoorSprite);
room.animatedDoors.push(doorTopSprite);
console.log(`Created animated door sprite at (${doorX}, ${doorY}) in room ${roomId} with door top`);
} catch (error) {
console.warn(`Failed to create animated door sprite:`, error);
// Fallback to a simple colored rectangle
const graphics = gameRef.add.graphics();
graphics.fillStyle(0x00ff00, 1); // Green color for open door
graphics.fillRect(-doorWidth/2, -doorHeight/2, doorWidth, doorHeight);
graphics.setPosition(doorX, doorY);
// Calculate the bottom of the door (where it meets the ground)
const doorBottomY = doorY + (TILE_SIZE * 2) / 2; // doorY is center, so add half height to get bottom
graphics.setDepth(doorBottomY + 0.45); // Bottom Y + door layer offset
if (!room.animatedDoors) {
room.animatedDoors = [];
}
room.animatedDoors.push(graphics);
console.log(`Created fallback animated door at (${doorX}, ${doorY}) in room ${roomId}`);
}
}
// Helper function to get the opposite direction
export function getOppositeDirection(direction) {
switch (direction) {
case 'north': return 'south';
case 'south': return 'north';
case 'east': return 'west';
case 'west': return 'east';
default: return direction;
}
}
// Function to check if player has crossed a door threshold
export function checkDoorTransitions(player) {
// Check cooldown first
const currentTime = Date.now();
if (currentTime - lastDoorTransitionTime < DOOR_TRANSITION_COOLDOWN) {
return null; // Still in cooldown
}
const playerBottomY = player.y + (player.height * player.scaleY) / 2;
let closestTransition = null;
let closestDistance = Infinity;
// Check all rooms for door transitions
Object.entries(rooms).forEach(([roomId, room]) => {
if (!room.doorSprites) return;
room.doorSprites.forEach(doorSprite => {
// Get door information from the sprite's custom properties
const doorInfo = doorSprite.doorInfo;
if (!doorInfo) return;
const { direction, connectedRoom } = doorInfo;
// Skip if this would transition to the current room
if (connectedRoom === window.currentPlayerRoom) {
return;
}
// Skip if this is the same transition we just made
if (lastDoorTransition === `${window.currentPlayerRoom}->${connectedRoom}`) {
return;
}
// Calculate door threshold based on direction
let doorThreshold = null;
const roomPosition = room.position;
const roomHeight = room.map.heightInPixels;
if (direction === 'north') {
// North door: threshold is 2 tiles down from top (bottom of door)
doorThreshold = roomPosition.y + TILE_SIZE * 2; // 1 tile from top + 1 more tile for door height
} else if (direction === 'south') {
// South door: threshold is 2 tiles up from bottom (top of door)
doorThreshold = roomPosition.y + roomHeight - TILE_SIZE * 2; // 1 tile from bottom + 1 more tile for door height
}
if (doorThreshold !== null) {
// Check if player has crossed the threshold
let shouldTransition = false;
if (direction === 'north' && playerBottomY <= doorThreshold) {
shouldTransition = true;
} else if (direction === 'south' && playerBottomY >= doorThreshold) {
shouldTransition = true;
}
if (shouldTransition) {
// Calculate distance to this door threshold
const distanceToThreshold = Math.abs(playerBottomY - doorThreshold);
// Only consider this transition if it's closer than any previous one
if (distanceToThreshold < closestDistance) {
closestDistance = distanceToThreshold;
closestTransition = connectedRoom;
console.log(`Player crossed ${direction} door threshold in ${roomId} -> ${connectedRoom} (current: ${window.currentPlayerRoom}, distance: ${distanceToThreshold.toFixed(2)})`);
}
}
}
});
});
// If a transition was detected, set the cooldown and track the transition
if (closestTransition) {
lastDoorTransitionTime = currentTime;
lastDoorTransition = `${window.currentPlayerRoom}->${closestTransition}`;
}
return closestTransition;
}
// Update door sprites visibility based on which rooms are revealed
export function updateDoorSpritesVisibility() {
const discoveredRooms = window.discoveredRooms || new Set();
console.log(`updateDoorSpritesVisibility called. Discovered rooms:`, Array.from(discoveredRooms));
Object.entries(rooms).forEach(([roomId, room]) => {
if (!room.doorSprites) return;
room.doorSprites.forEach(doorSprite => {
// Get the door sprite's bounds (it covers 2 tiles vertically)
const doorSpriteBounds = {
x: doorSprite.x - TILE_SIZE/2, // Left edge of door sprite (center origin)
y: doorSprite.y - TILE_SIZE, // Top edge of door sprite (center origin)
width: TILE_SIZE, // Door sprite width
height: TILE_SIZE * 2 // Door sprite height (2 tiles)
};
// Check if this room is revealed (doors should be visible if their room is visible)
const thisRoomRevealed = discoveredRooms.has(roomId);
// Check how many other revealed rooms this door overlaps with
let overlappingRevealedRooms = 0;
Object.entries(rooms).forEach(([otherRoomId, otherRoom]) => {
if (!discoveredRooms.has(otherRoomId)) return; // Skip unrevealed rooms
const otherRoomBounds = {
x: otherRoom.position.x,
y: otherRoom.position.y,
width: otherRoom.map.widthInPixels,
height: otherRoom.map.heightInPixels
};
// Check if door sprite bounds overlap with this revealed room
if (boundsOverlap(doorSpriteBounds, otherRoomBounds)) {
overlappingRevealedRooms++;
}
});
// Door should be visible if its room is revealed OR if it overlaps with any revealed room
const shouldBeVisible = thisRoomRevealed || overlappingRevealedRooms > 0;
console.log(`Door sprite at (${doorSprite.x}, ${doorSprite.y}) in room ${roomId}:`);
console.log(` This room revealed: ${thisRoomRevealed}`);
console.log(` Overlapping revealed rooms: ${overlappingRevealedRooms}`);
console.log(` Should be visible: ${shouldBeVisible}`);
if (shouldBeVisible) {
doorSprite.setVisible(true);
doorSprite.setAlpha(1);
} else {
doorSprite.setVisible(false);
doorSprite.setAlpha(0);
}
});
});
}
// Helper function to check if two rectangles overlap
function boundsOverlap(rect1, rect2) {
return rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y;
}
// Export for global access
window.updateDoorSpritesVisibility = updateDoorSpritesVisibility;
window.checkDoorTransitions = checkDoorTransitions;
// Export functions for use by other modules
export { unlockDoor };

View File

@@ -1,6 +1,7 @@
// Object interaction system
import { INTERACTION_RANGE, INTERACTION_RANGE_SQ, INTERACTION_CHECK_INTERVAL, TILE_SIZE, DOOR_ALIGN_OVERLAP } from '../utils/constants.js?v=7';
import { rooms } from '../core/rooms.js?v=16';
import { unlockDoor } from './doors.js';
// Helper function to check if two rectangles overlap
function boundsOverlap(rect1, rect2) {
@@ -12,6 +13,190 @@ function boundsOverlap(rect1, rect2) {
let gameRef = null;
// Global key-lock mapping system
// This ensures each key matches exactly one lock in the game
window.keyLockMappings = window.keyLockMappings || {};
// Predefined lock configurations for the game
// Each lock has a unique ID and pin configuration
const PREDEFINED_LOCK_CONFIGS = {
'ceo_briefcase_lock': {
id: 'ceo_briefcase_lock',
pinCount: 4,
pinHeights: [32, 28, 35, 30], // Specific pin heights for CEO briefcase
difficulty: 'medium'
},
'office_drawer_lock': {
id: 'office_drawer_lock',
pinCount: 3,
pinHeights: [25, 30, 28],
difficulty: 'easy'
},
'server_room_lock': {
id: 'server_room_lock',
pinCount: 5,
pinHeights: [40, 35, 38, 32, 36],
difficulty: 'hard'
},
'storage_cabinet_lock': {
id: 'storage_cabinet_lock',
pinCount: 4,
pinHeights: [29, 33, 27, 31],
difficulty: 'medium'
}
};
// Function to assign keys to locks based on scenario definitions
function assignKeysToLocks() {
console.log('Assigning keys to locks based on scenario definitions...');
// Get all keys from inventory
const playerKeys = window.inventory?.items?.filter(item =>
item && item.scenarioData &&
item.scenarioData.type === 'key'
) || [];
console.log(`Found ${playerKeys.length} keys in inventory`);
// Get all rooms from the current scenario
const rooms = window.gameState?.scenario?.rooms || {};
console.log(`Found ${Object.keys(rooms).length} rooms in scenario`);
// Find all locks that require keys
const keyLocks = [];
Object.entries(rooms).forEach(([roomId, roomData]) => {
if (roomData.locked && roomData.lockType === 'key' && roomData.requires) {
keyLocks.push({
roomId: roomId,
requiredKeyId: roomData.requires,
roomName: roomData.type || roomId
});
}
// Also check objects within rooms for key locks
if (roomData.objects) {
roomData.objects.forEach((obj, objIndex) => {
if (obj.locked && obj.lockType === 'key' && obj.requires) {
keyLocks.push({
roomId: roomId,
objectIndex: objIndex,
requiredKeyId: obj.requires,
objectName: obj.name || obj.type
});
}
});
}
});
console.log(`Found ${keyLocks.length} key locks in scenario:`, keyLocks);
// Create mappings based on scenario definitions
keyLocks.forEach(lock => {
const keyId = lock.requiredKeyId;
// Find the key in player inventory
const key = playerKeys.find(k => k.scenarioData.key_id === keyId);
if (key) {
// Create a lock configuration for this specific lock
const lockConfig = {
id: `${lock.roomId}_${lock.objectIndex !== undefined ? `obj_${lock.objectIndex}` : 'room'}`,
pinCount: 4, // Default pin count
pinHeights: generatePinHeightsForLock(lock.roomId, keyId), // Generate consistent pin heights
difficulty: 'medium'
};
// Store the mapping
window.keyLockMappings[keyId] = {
lockId: lockConfig.id,
lockConfig: lockConfig,
keyName: key.scenarioData.name,
roomId: lock.roomId,
objectIndex: lock.objectIndex,
lockName: lock.objectName || lock.roomName
};
console.log(`Assigned key "${key.scenarioData.name}" (${keyId}) to lock in ${lock.roomName}${lock.objectName ? ` - ${lock.objectName}` : ''}`);
} else {
console.warn(`Key "${keyId}" required by lock in ${lock.roomName}${lock.objectName ? ` - ${lock.objectName}` : ''} not found in inventory`);
}
});
console.log('Key-lock mappings based on scenario:', window.keyLockMappings);
}
// Function to generate consistent pin heights for a lock based on room and key
function generatePinHeightsForLock(roomId, keyId) {
// Use a deterministic seed based on room and key IDs
const seed = (roomId + keyId).split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
const random = (min, max) => {
const x = Math.sin(seed++) * 10000;
return Math.floor((x - Math.floor(x)) * (max - min + 1)) + min;
};
const pinHeights = [];
for (let i = 0; i < 4; i++) {
pinHeights.push(25 + random(0, 37)); // 25-62 range
}
return pinHeights;
}
// Function to check if a key matches a specific lock
function doesKeyMatchLock(keyId, lockId) {
if (!window.keyLockMappings || !window.keyLockMappings[keyId]) {
return false;
}
const mapping = window.keyLockMappings[keyId];
return mapping.lockId === lockId;
}
// Function to get the lock ID that a key is assigned to
function getKeyAssignedLock(keyId) {
if (!window.keyLockMappings || !window.keyLockMappings[keyId]) {
return null;
}
return window.keyLockMappings[keyId].lockId;
}
// Console helper functions for testing
window.reassignKeysToLocks = function() {
// Clear existing mappings
window.keyLockMappings = {};
assignKeysToLocks();
console.log('Key-lock mappings reassigned based on current scenario');
};
window.showKeyLockMappings = function() {
console.log('Current key-lock mappings:', window.keyLockMappings);
console.log('Available lock configurations:', PREDEFINED_LOCK_CONFIGS);
// Show scenario-based mappings
if (window.gameState?.scenario?.rooms) {
console.log('Current scenario rooms:', Object.keys(window.gameState.scenario.rooms));
}
};
window.testKeyLockMatch = function(keyId, lockId) {
const matches = doesKeyMatchLock(keyId, lockId);
console.log(`Key "${keyId}" ${matches ? 'MATCHES' : 'DOES NOT MATCH'} lock "${lockId}"`);
return matches;
};
// Function to reinitialize mappings when scenario changes
window.initializeKeyLockMappings = function() {
console.log('Initializing key-lock mappings for current scenario...');
window.keyLockMappings = {};
assignKeysToLocks();
};
// Initialize key-lock mappings when the game starts
if (window.inventory && window.inventory.items) {
assignKeysToLocks();
}
export function setGameInstance(gameInstance) {
gameRef = gameInstance;
}
@@ -462,7 +647,7 @@ function removeFromInventory(item) {
}
}
function handleUnlock(lockable, type) {
export function handleUnlock(lockable, type) {
console.log('UNLOCK ATTEMPT');
// Get lock requirements based on type
@@ -676,7 +861,7 @@ function handleUnlock(lockable, type) {
}
}
function getLockRequirementsForDoor(doorSprite) {
export function getLockRequirementsForDoor(doorSprite) {
const doorWorldX = doorSprite.x;
const doorWorldY = doorSprite.y;
@@ -745,14 +930,8 @@ function getLockRequirementsForItem(item) {
function unlockTarget(lockable, type, layer) {
if (type === 'door') {
// After unlocking, open the door
// Find the room that contains this door sprite
const room = Object.values(rooms).find(r =>
r.doorSprites && r.doorSprites.includes(lockable)
);
if (room) {
openDoor(lockable, room);
}
// After unlocking, use the proper door unlock function
unlockDoor(lockable);
} else {
// Handle item unlocking
if (lockable.scenarioData) {
@@ -773,13 +952,7 @@ function unlockTarget(lockable, type, layer) {
console.log(`${type} unlocked successfully`);
}
// This function is no longer needed - door unlocking is handled by the minigame system
// and door opening is handled by openDoor()
function unlockDoor(doorTile, doorsLayer) {
console.log('unlockDoor called - this should not happen');
// The unlock process is handled by the minigame system
// When unlock is successful, unlockTarget() calls openDoor()
}
// Legacy unlockDoor function removed - door unlocking is now handled by doors.js
// Store door zones globally so we can manage them
window.doorZones = window.doorZones || new Map();
@@ -982,6 +1155,104 @@ function startLockpickingMinigame(lockable, scene, difficulty = 'medium', callba
});
}
// Function to generate key cuts that match a specific lock's pin configuration
function generateKeyCutsForLock(key, lockable) {
const keyId = key.scenarioData.key_id;
// Check if this key has a predefined lock assignment
if (window.keyLockMappings && window.keyLockMappings[keyId]) {
const mapping = window.keyLockMappings[keyId];
const lockConfig = mapping.lockConfig;
console.log(`Generating cuts for key "${key.scenarioData.name}" assigned to lock "${mapping.lockId}"`);
// Generate cuts based on the assigned lock's pin configuration
const cuts = [];
const pinHeights = lockConfig.pinHeights || [];
for (let i = 0; i < lockConfig.pinCount; i++) {
const keyPinLength = pinHeights[i] || 30; // Use predefined pin height
// Calculate cut depth with INVERSE relationship to key pin length
// Longer key pins need shallower cuts (less lift required)
// Shorter key pins need deeper cuts (more lift required)
// Based on the lockpicking minigame formula:
// Cut depth = key pin length - gap from key blade top to shear line
const keyBladeTop_world = 175; // Key blade top position
const shearLine_world = 155; // Shear line position
const gapFromKeyBladeTopToShearLine = keyBladeTop_world - shearLine_world; // 20
// Calculate the required cut depth
const cutDepth_needed = keyPinLength - gapFromKeyBladeTopToShearLine;
// Clamp to valid range (0 to 110, which is key blade height)
const clampedCutDepth = Math.max(0, Math.min(110, cutDepth_needed));
cuts.push(Math.round(clampedCutDepth));
console.log(`Pin ${i}: keyPinLength=${keyPinLength}, cutDepth=${clampedCutDepth} (gap=${gapFromKeyBladeTopToShearLine})`);
}
console.log(`Generated cuts for key ${keyId} (assigned to ${mapping.lockId}):`, cuts);
return cuts;
}
// Fallback: Try to get the lock's pin configuration from the minigame framework
let lockConfig = null;
const lockId = lockable.scenarioData?.lockId || lockable.id || 'default_lock';
if (window.lockConfigurations && window.lockConfigurations[lockId]) {
lockConfig = window.lockConfigurations[lockId];
}
// If no saved config, generate a default configuration
if (!lockConfig) {
console.log(`No predefined mapping for key ${keyId} and no saved lock configuration for ${lockId}, generating default cuts`);
// Generate random cuts based on the key_id for consistency
let seed = key.scenarioData.key_id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
const random = (min, max) => {
const x = Math.sin(seed++) * 10000;
return Math.floor((x - Math.floor(x)) * (max - min + 1)) + min;
};
const cuts = [];
const numCuts = key.scenarioData.pinCount || 4;
for (let i = 0; i < numCuts; i++) {
cuts.push(random(20, 80)); // Random cuts between 20-80
}
return cuts;
}
// Generate cuts based on the lock's actual pin configuration
console.log(`Generating key cuts for lock ${lockId} with config:`, lockConfig);
const cuts = [];
const pinHeights = lockConfig.pinHeights || [];
// Generate cuts that will work with the lock's pin heights
for (let i = 0; i < lockConfig.pinCount; i++) {
const keyPinLength = pinHeights[i] || (25 + Math.random() * 37.5); // Default if missing
// Calculate cut depth with INVERSE relationship to key pin length
// Based on the lockpicking minigame formula:
// Cut depth = key pin length - gap from key blade top to shear line
const keyBladeTop_world = 175; // Key blade top position
const shearLine_world = 155; // Shear line position
const gapFromKeyBladeTopToShearLine = keyBladeTop_world - shearLine_world; // 20
// Calculate the required cut depth
const cutDepth_needed = keyPinLength - gapFromKeyBladeTopToShearLine;
// Clamp to valid range (0 to 110, which is key blade height)
const clampedCutDepth = Math.max(0, Math.min(110, cutDepth_needed));
cuts.push(Math.round(clampedCutDepth));
}
console.log(`Generated cuts for key ${key.scenarioData.key_id}:`, cuts);
return cuts;
}
function startKeySelectionMinigame(lockable, type, playerKeys, requiredKeyId) {
console.log('Starting key selection minigame', { playerKeys, requiredKeyId });
@@ -1004,37 +1275,95 @@ function startKeySelectionMinigame(lockable, type, playerKeys, requiredKeyId) {
window.MinigameFramework.init(window.game);
}
// Determine the lock ID for this lockable based on scenario data
let lockId = null;
// Try to find the lock ID from the scenario data
if (lockable.scenarioData?.requires) {
// This is a key lock, find which key it requires
const requiredKeyId = lockable.scenarioData.requires;
// Find the mapping for this key to get the lock ID
if (window.keyLockMappings && window.keyLockMappings[requiredKeyId]) {
lockId = window.keyLockMappings[requiredKeyId].lockId;
console.log(`Found lock ID "${lockId}" for key "${requiredKeyId}"`);
}
}
// Fallback to default lock ID
if (!lockId) {
lockId = lockable.scenarioData?.lockId || lockable.id || 'default_lock';
console.log(`Using fallback lock ID "${lockId}"`);
}
// Find the key that matches this lock
const matchingKey = playerKeys.find(key => doesKeyMatchLock(key.scenarioData.key_id, lockId));
let keysToShow = playerKeys;
if (matchingKey) {
console.log(`Found matching key "${matchingKey.scenarioData.name}" for lock "${lockId}"`);
// For now, show all keys so player has to figure out which one works
// In the future, you could show only the matching key or give hints
} else {
console.log(`No matching key found for lock "${lockId}", showing all keys`);
}
// Convert inventory keys to the format expected by the minigame
const inventoryKeys = playerKeys.map(key => {
const inventoryKeys = keysToShow.map(key => {
// Generate cuts data if not present
let cuts = key.scenarioData.cuts;
if (!cuts) {
// Generate random cuts based on the key_id for consistency
const seed = key.scenarioData.key_id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
const random = (min, max) => {
const x = Math.sin(seed++) * 10000;
return Math.floor((x - Math.floor(x)) * (max - min + 1)) + min;
};
cuts = [];
for (let i = 0; i < 5; i++) {
cuts.push(random(20, 80)); // Random cuts between 20-80
}
// Generate cuts that match the lock's pin configuration
cuts = generateKeyCutsForLock(key, lockable);
}
return {
id: key.scenarioData.key_id,
name: key.scenarioData.name,
cuts: cuts,
pinCount: key.scenarioData.pinCount || 5
pinCount: key.scenarioData.pinCount || 4, // Default to 4 pins to match most locks
matchesLock: doesKeyMatchLock(key.scenarioData.key_id, lockId) // Add flag for matching
};
});
// Determine which lock configuration to use for this lockable
let lockConfig = null;
// First, try to find the lock configuration from scenario-based mappings
if (lockable.scenarioData?.requires) {
const requiredKeyId = lockable.scenarioData.requires;
if (window.keyLockMappings && window.keyLockMappings[requiredKeyId]) {
lockConfig = window.keyLockMappings[requiredKeyId].lockConfig;
console.log(`Using scenario-based lock configuration for key "${requiredKeyId}":`, lockConfig);
}
}
// Fallback to predefined configurations
if (!lockConfig && PREDEFINED_LOCK_CONFIGS[lockId]) {
lockConfig = PREDEFINED_LOCK_CONFIGS[lockId];
console.log(`Using predefined lock configuration for ${lockId}:`, lockConfig);
}
// Final fallback to default configuration
if (!lockConfig) {
lockConfig = {
id: lockId,
pinCount: 4,
pinHeights: [30, 28, 32, 29],
difficulty: 'medium'
};
console.log(`Using default lock configuration for ${lockId}:`, lockConfig);
}
// Start the key selection minigame
window.MinigameFramework.startMinigame('lockpicking', null, {
keyMode: true,
skipStartingKey: true,
lockable: lockable,
lockId: lockId,
pinCount: lockConfig.pinCount,
predefinedPinHeights: lockConfig.pinHeights, // Pass the predefined pin heights
difficulty: lockConfig.difficulty,
cancelText: 'Close',
onComplete: (success, result) => {
if (success) {
@@ -1049,9 +1378,25 @@ function startKeySelectionMinigame(lockable, type, playerKeys, requiredKeyId) {
});
// Start with key selection using inventory keys
// Wait for the minigame to be fully initialized and lock configuration to be saved
setTimeout(() => {
if (window.MinigameFramework.currentMinigame && window.MinigameFramework.currentMinigame.startWithKeySelection) {
window.MinigameFramework.currentMinigame.startWithKeySelection(inventoryKeys, requiredKeyId);
// Regenerate keys with the actual lock configuration now that it's been created
const updatedInventoryKeys = playerKeys.map(key => {
let cuts = key.scenarioData.cuts;
if (!cuts) {
cuts = generateKeyCutsForLock(key, lockable);
}
return {
id: key.scenarioData.key_id,
name: key.scenarioData.name,
cuts: cuts,
pinCount: key.scenarioData.pinCount || 4
};
});
window.MinigameFramework.currentMinigame.startWithKeySelection(updatedInventoryKeys, requiredKeyId);
}
}, 500);
}
@@ -1152,7 +1497,7 @@ function startDustingMinigame(item) {
item.scene = window.game;
// Start the dusting minigame
window.MinigameFramework.startMinigame('dusting', {
window.MinigameFramework.startMinigame('dusting', null, {
item: item,
scene: item.scene,
onComplete: (success, result) => {

View File

@@ -0,0 +1,280 @@
/**
* OBJECT PHYSICS SYSTEM
* =====================
*
* Handles physics bodies, collision setup, and object behavior for chairs and other objects.
* Separated from rooms.js for better modularity and maintainability.
*/
import { TILE_SIZE } from '../utils/constants.js';
let gameRef = null;
let rooms = null;
// Initialize object physics system
export function initializeObjectPhysics(gameInstance, roomsRef) {
gameRef = gameInstance;
rooms = roomsRef;
}
// Set up collision detection between chairs and other objects
export function setupChairCollisions(chair) {
if (!chair || !chair.body) return;
// Collision with other chairs
if (window.chairs) {
window.chairs.forEach(otherChair => {
if (otherChair !== chair && otherChair.body) {
gameRef.physics.add.collider(chair, otherChair);
}
});
}
// Collision with tables and other static objects
Object.values(rooms).forEach(room => {
if (room.objects) {
Object.values(room.objects).forEach(obj => {
if (obj !== chair && obj.body && obj.body.immovable) {
gameRef.physics.add.collider(chair, obj);
}
});
}
});
// Collision with wall collision boxes
Object.values(rooms).forEach(room => {
if (room.wallCollisionBoxes) {
room.wallCollisionBoxes.forEach(wallBox => {
if (wallBox.body) {
gameRef.physics.add.collider(chair, wallBox);
}
});
}
});
// Collision with closed door sprites
Object.values(rooms).forEach(room => {
if (room.doorSprites) {
room.doorSprites.forEach(doorSprite => {
// Only collide with closed doors (doors that haven't been opened)
if (doorSprite.body && doorSprite.body.immovable) {
gameRef.physics.add.collider(chair, doorSprite);
}
});
}
});
}
// Set up collisions between existing chairs and new room objects
export function setupExistingChairsWithNewRoom(roomId) {
if (!window.chairs) return;
const room = rooms[roomId];
if (!room) return;
// Collision with new room's tables and static objects
if (room.objects) {
Object.values(room.objects).forEach(obj => {
if (obj.body && obj.body.immovable) {
window.chairs.forEach(chair => {
if (chair.body) {
gameRef.physics.add.collider(chair, obj);
}
});
}
});
}
// Collision with new room's wall collision boxes
if (room.wallCollisionBoxes) {
room.wallCollisionBoxes.forEach(wallBox => {
if (wallBox.body) {
window.chairs.forEach(chair => {
if (chair.body) {
gameRef.physics.add.collider(chair, wallBox);
}
});
}
});
}
// Collision with new room's door sprites
if (room.doorSprites) {
room.doorSprites.forEach(doorSprite => {
// Only collide with closed doors (doors that haven't been opened)
if (doorSprite.body && doorSprite.body.immovable) {
window.chairs.forEach(chair => {
if (chair.body) {
gameRef.physics.add.collider(chair, doorSprite);
}
});
}
});
}
}
// Calculate chair spin direction based on contact point
export function calculateChairSpinDirection(player, chair) {
if (!chair.isSwivelChair) return;
// Get relative position of player to chair SPRITE center (not collision box)
const chairSpriteCenterX = chair.x + chair.width / 2;
const chairSpriteCenterY = chair.y + chair.height / 2;
const playerX = player.x + player.width / 2;
const playerY = player.y + player.height / 2;
// Calculate offset from chair sprite center
const offsetX = playerX - chairSpriteCenterX;
const offsetY = playerY - chairSpriteCenterY;
// Calculate distance from center using sprite dimensions (not collision box)
const distanceFromCenter = Math.sqrt(offsetX * offsetX + offsetY * offsetY);
// Use the larger sprite dimension for maxDistance to make center area larger
const maxDistance = Math.max(chair.width, chair.height) / 2;
const centerRatio = distanceFromCenter / maxDistance;
// Determine spin based on distance from center (EXTREMELY large center area)
if (centerRatio > 1.2) { // 120% from center - edge hit (strong spin) - ONLY VERY EDGES
// Determine spin direction based on which side of chair player is on
if (Math.abs(offsetX) > Math.abs(offsetY)) {
// Horizontal contact - spin based on X offset
chair.spinDirection = offsetX > 0 ? 1 : -1; // Right side = clockwise, left side = counter-clockwise
} else {
// Vertical contact - spin based on Y offset and player movement
const playerVelocityX = player.body.velocity.x;
if (Math.abs(playerVelocityX) > 10) {
// Player is moving horizontally - use that for spin direction
chair.spinDirection = playerVelocityX > 0 ? 1 : -1;
} else {
// Use Y offset for spin direction
chair.spinDirection = offsetY > 0 ? 1 : -1;
}
}
// Strong spin for edge hits
const spinIntensity = Math.min(centerRatio, 1.0);
chair.maxRotationSpeed = 0.15 * spinIntensity;
chair.rotationSpeed = Math.max(chair.rotationSpeed, 0.05); // Strong rotation start
} else if (centerRatio > 0.8) { // 80-120% from center - moderate hit
// Moderate spin
if (Math.abs(offsetX) > Math.abs(offsetY)) {
chair.spinDirection = offsetX > 0 ? 1 : -1;
} else {
const playerVelocityX = player.body.velocity.x;
chair.spinDirection = Math.abs(playerVelocityX) > 10 ? (playerVelocityX > 0 ? 1 : -1) : (offsetY > 0 ? 1 : -1);
}
const spinIntensity = centerRatio * 0.3; // Reduced intensity
chair.maxRotationSpeed = 0.06 * spinIntensity;
chair.rotationSpeed = Math.max(chair.rotationSpeed, 0.015); // Moderate rotation start
} else { // 0-80% from center - center hit (minimal spin) - MASSIVE CENTER AREA
// Very minimal or no spin for center hits
chair.spinDirection = 0;
chair.maxRotationSpeed = 0.01; // Very slow spin
chair.rotationSpeed = Math.max(chair.rotationSpeed, 0.002); // Minimal rotation start
}
}
// Update swivel chair rotation based on movement
export function updateSwivelChairRotation() {
if (!window.chairs) return;
window.chairs.forEach(chair => {
if (!chair.hasWheels || !chair.body) return;
// Update chair depth based on current position (for all chairs with wheels)
updateSpriteDepth(chair, chair.elevation || 0);
// Only process rotation for swivel chairs
if (!chair.isSwivelChair) return;
// Calculate movement speed
const velocity = Math.sqrt(
chair.body.velocity.x * chair.body.velocity.x +
chair.body.velocity.y * chair.body.velocity.y
);
// Update rotation speed based on movement
if (velocity > 10) {
// Chair is moving - increase rotation speed (slower acceleration)
chair.rotationSpeed = Math.min(chair.rotationSpeed + 0.01, chair.maxRotationSpeed);
// If no spin direction set, set a default one for testing
if (chair.spinDirection === 0) {
chair.spinDirection = 1; // Default to clockwise
}
} else {
// Chair is slowing down - decrease rotation speed (slower deceleration)
chair.rotationSpeed = Math.max(chair.rotationSpeed - 0.005, 0);
// Reset spin direction when chair stops moving
if (chair.rotationSpeed < 0.01) {
chair.spinDirection = 0;
}
}
// Update frame based on rotation speed and direction
if (chair.rotationSpeed > 0.01) {
// Apply spin direction to rotation
const rotationDelta = chair.rotationSpeed * chair.spinDirection;
chair.currentFrame += rotationDelta;
// Handle frame wrapping (8 frames total: 0-7)
if (chair.currentFrame >= 8) {
chair.currentFrame = 0; // Loop back to first frame
} else if (chair.currentFrame < 0) {
chair.currentFrame = 7; // Loop back to last frame (for counter-clockwise)
}
// Set the texture based on current frame and chair type
const frameIndex = Math.floor(chair.currentFrame) + 1; // Convert to 1-based index
let newTexture;
// Determine texture prefix based on original texture
if (chair.originalTexture && chair.originalTexture.startsWith('chair-exec-rotate')) {
newTexture = `chair-exec-rotate${frameIndex}`;
} else if (chair.originalTexture && chair.originalTexture.startsWith('chair-white-1-rotate')) {
newTexture = `chair-white-1-rotate${frameIndex}`;
} else if (chair.originalTexture && chair.originalTexture.startsWith('chair-white-2-rotate')) {
newTexture = `chair-white-2-rotate${frameIndex}`;
} else {
// Fallback to exec chair if original texture is unknown
newTexture = `chair-exec-rotate${frameIndex}`;
}
// Check if texture exists before setting
if (gameRef.textures.exists(newTexture)) {
chair.setTexture(newTexture);
} else {
console.warn(`Texture not found: ${newTexture}`);
}
}
});
}
// Reusable function to update sprite depth based on Y position and elevation
export function updateSpriteDepth(sprite, elevation = 0) {
if (!sprite || !sprite.active) return;
// Get the bottom of the sprite (feet position)
const spriteBottomY = sprite.y + (sprite.height * sprite.scaleY);
// Calculate depth: world Y position + layer offset + elevation
const spriteDepth = spriteBottomY + 0.5 + elevation;
// Set the sprite depth
sprite.setDepth(spriteDepth);
}
// Export for global access
window.setupChairCollisions = setupChairCollisions;
window.setupExistingChairsWithNewRoom = setupExistingChairsWithNewRoom;
window.calculateChairSpinDirection = calculateChairSpinDirection;
window.updateSwivelChairRotation = updateSwivelChairRotation;
window.updateSpriteDepth = updateSpriteDepth;

View File

@@ -0,0 +1,268 @@
/**
* PLAYER EFFECTS SYSTEM
* =====================
*
* Handles visual effects and animations triggered by player interactions.
* Separated from rooms.js for better modularity and maintainability.
*/
import { TILE_SIZE } from '../utils/constants.js';
let gameRef = null;
let rooms = null;
// Player bump effect variables
let playerBumpTween = null;
let isPlayerBumping = false;
let lastPlayerPosition = { x: 0, y: 0 };
let steppedOverItems = new Set(); // Track items we've already stepped over
let playerVisualOverlay = null; // Visual overlay for hop effect
let lastHopTime = 0; // Track when last hop occurred
const HOP_COOLDOWN = 300; // 300ms cooldown between hops
// Initialize player effects system
export function initializePlayerEffects(gameInstance, roomsRef) {
gameRef = gameInstance;
rooms = roomsRef;
}
// Function to create player bump effect when walking over items
export function createPlayerBumpEffect() {
if (!window.player || isPlayerBumping) return;
// Check cooldown to prevent double hopping
const currentTime = Date.now();
if (currentTime - lastHopTime < HOP_COOLDOWN) {
return; // Still in cooldown, skip this frame
}
const player = window.player;
const currentX = player.x;
const currentY = player.y;
// Check if player has moved significantly (to detect stepping over items)
const hasMoved = Math.abs(currentX - lastPlayerPosition.x) > 5 ||
Math.abs(currentY - lastPlayerPosition.y) > 5;
if (!hasMoved) return;
// Update last position
lastPlayerPosition = { x: currentX, y: currentY };
// Check all rooms for floor items
Object.entries(rooms).forEach(([roomId, room]) => {
if (!room.objects) return;
Object.values(room.objects).forEach(obj => {
if (!obj.visible || !obj.scenarioData) return;
// Create unique identifier for this item
const itemId = `${roomId}_${obj.objectId || obj.name}_${obj.x}_${obj.y}`;
// Skip if we've already stepped over this item recently
if (steppedOverItems.has(itemId)) return;
// Check if this is a floor item (not furniture)
const isFloorItem = obj.scenarioData.type &&
!obj.scenarioData.type.includes('table') &&
!obj.scenarioData.type.includes('chair') &&
!obj.scenarioData.type.includes('desk') &&
!obj.scenarioData.type.includes('safe') &&
!obj.scenarioData.type.includes('workstation');
if (!isFloorItem) return;
// Check if player collision box intersects with bottom portion of item
const playerCollisionLeft = currentX - (player.body.width / 2);
const playerCollisionRight = currentX + (player.body.width / 2);
const playerCollisionTop = currentY - (player.body.height / 2);
const playerCollisionBottom = currentY + (player.body.height / 2);
// Focus on bottom 1/3 of the item sprite
const itemBottomStart = obj.y + (obj.height * 2/3); // Start of bottom third
const itemBottomEnd = obj.y + obj.height; // Bottom of item
const itemLeft = obj.x;
const itemRight = obj.x + obj.width;
// Check if player collision box intersects with bottom third of item
if (playerCollisionRight >= itemLeft &&
playerCollisionLeft <= itemRight &&
playerCollisionBottom >= itemBottomStart &&
playerCollisionTop <= itemBottomEnd) {
// Player stepped over a floor item - create one-time hop effect
steppedOverItems.add(itemId);
lastHopTime = currentTime; // Update hop time
// Remove from set after 2 seconds to allow re-triggering
setTimeout(() => {
steppedOverItems.delete(itemId);
}, 2000);
// Create one-time hop effect
if (playerBumpTween) {
playerBumpTween.destroy();
}
isPlayerBumping = true;
// Create hop effect using visual overlay
if (playerBumpTween) {
playerBumpTween.destroy();
}
// Create a visual overlay sprite that follows the player
if (playerVisualOverlay) {
playerVisualOverlay.destroy();
}
playerVisualOverlay = gameRef.add.sprite(player.x, player.y, player.texture.key);
playerVisualOverlay.setFrame(player.frame.name);
playerVisualOverlay.setScale(player.scaleX, player.scaleY);
playerVisualOverlay.setFlipX(player.flipX); // Copy horizontal flip state
playerVisualOverlay.setFlipY(player.flipY); // Copy vertical flip state
playerVisualOverlay.setDepth(player.depth + 1);
playerVisualOverlay.setAlpha(0.8);
// Hide the original player temporarily
player.setAlpha(0);
// Always hop upward - negative Y values move sprite up on screen
const hopHeight = -15; // Consistent upward hop
// Debug: Log the hop details
console.log(`Hop triggered - Player Y: ${player.y}, Overlay Y: ${playerVisualOverlay.y}, Hop Height: ${hopHeight}, Target Y: ${playerVisualOverlay.y + hopHeight}`);
console.log(`Player movement - DeltaX: ${currentX - lastPlayerPosition.x}, DeltaY: ${currentY - lastPlayerPosition.y}`);
// Start the hop animation with a simple up-down motion
playerBumpTween = gameRef.tweens.add({
targets: { hopOffset: 0 },
hopOffset: hopHeight,
duration: 120,
ease: 'Power2',
yoyo: true,
onUpdate: (tween) => {
if (playerVisualOverlay && playerVisualOverlay.active) {
// Apply the hop offset to the current player position
playerVisualOverlay.setY(player.y + tween.getValue());
}
},
onComplete: () => {
// Clean up overlay and restore player
if (playerVisualOverlay) {
playerVisualOverlay.destroy();
playerVisualOverlay = null;
}
player.setAlpha(1); // Restore player visibility
isPlayerBumping = false;
playerBumpTween = null;
}
});
// Make overlay follow player movement during hop
const followPlayer = () => {
if (playerVisualOverlay && playerVisualOverlay.active) {
// Update X position and flip states, Y is handled by the tween
playerVisualOverlay.setX(player.x);
playerVisualOverlay.setFlipX(player.flipX); // Update flip state
playerVisualOverlay.setFlipY(player.flipY); // Update flip state
}
};
// Update overlay position every frame during hop
const followInterval = setInterval(() => {
if (!playerVisualOverlay || !playerVisualOverlay.active) {
clearInterval(followInterval);
return;
}
followPlayer();
}, 16); // ~60fps
// Clean up interval when hop completes
setTimeout(() => {
clearInterval(followInterval);
}, 240); // Slightly longer than animation duration
}
});
});
}
// Create plant sway effect when player walks through
export function createPlantSwayEffect() {
if (!window.player) return;
const player = window.player;
const currentX = player.x;
const currentY = player.y;
// Check if player is moving (has velocity)
const isMoving = Math.abs(player.body.velocity.x) > 10 || Math.abs(player.body.velocity.y) > 10;
if (!isMoving) return;
// Check all rooms for plants
Object.entries(rooms).forEach(([roomId, room]) => {
if (!room.objects) return;
Object.values(room.objects).forEach(obj => {
if (!obj.visible || !obj.canSway) return;
// Check if player is near the plant (within 40 pixels)
const distance = Phaser.Math.Distance.Between(currentX, currentY, obj.x + obj.width/2, obj.y + obj.height/2);
if (distance < 40 && !obj.isSwaying) {
obj.isSwaying = true;
// Create sway effect using displacement FX
// This creates a realistic distortion effect while keeping the base stationary
const swayIntensity = 0.05; // Increased intensity for more dramatic motion
const swayDuration = Phaser.Math.Between(400, 600); // Half the time - much faster animation
// Calculate sway direction based on player position relative to plant
const playerDirection = currentX > obj.x + obj.width/2 ? 1 : -1;
const displacementX = playerDirection * swayIntensity;
const displacementY = (Math.random() - 0.5) * swayIntensity * 0.8; // More vertical movement
// Create a complex sway animation using displacement
const swayTween = gameRef.tweens.add({
targets: obj.displacementFX,
x: displacementX,
y: displacementY,
duration: swayDuration / 3,
ease: 'Sine.easeInOut',
yoyo: true,
onComplete: () => {
// Second sway phase with opposite direction
gameRef.tweens.add({
targets: obj.displacementFX,
x: -displacementX * 0.8, // More dramatic opposite movement
y: -displacementY * 0.8,
duration: swayDuration / 3,
ease: 'Sine.easeInOut',
yoyo: true,
onComplete: () => {
// Final settle phase - return to original state
gameRef.tweens.add({
targets: obj.displacementFX,
x: 0.01, // Slightly higher default displacement
y: 0.01, // Slightly higher default displacement
duration: swayDuration / 3,
ease: 'Sine.easeOut',
onComplete: () => {
obj.isSwaying = false;
}
});
}
});
}
});
console.log(`Plant ${obj.name} swaying with intensity ${swayIntensity}, direction ${playerDirection}`);
}
});
});
}
// Export for global access
window.createPlayerBumpEffect = createPlayerBumpEffect;
window.createPlantSwayEffect = createPlantSwayEffect;